Merge remote-tracking branch 'origin/develop' into feature/account-provider-selection

This commit is contained in:
Jenna Vassar
2026-08-17 11:18:46 -07:00
248 changed files with 4184 additions and 1872 deletions
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"مشاركة بيانات استخدام مجهولة لمساعدتنا على تتبع المشاكل."</string>
<string name="screen_analytics_settings_read_terms">"يمكنك قراءة كافة شروطنا %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"هنا"</string>
<string name="screen_analytics_settings_share_data">"مشاركة البيانات الإحصائية"</string>
</resources>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string>
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam ulepszać aplikację."</string>
<string name="screen_analytics_settings_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"لن نسجّل أو ننشئ ملفات شخصية لأي معلومات شخصية"</string>
<string name="screen_analytics_prompt_help_us_improve">"مشاركة بيانات استخدام مجهولة لمساعدتنا على تتبع المشاكل."</string>
<string name="screen_analytics_prompt_read_terms">"يمكنك قراءة كافة شروطنا %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"هنا"</string>
<string name="screen_analytics_prompt_settings">"يمكنك إطفاء هذا الخيار في أي وقت"</string>
<string name="screen_analytics_prompt_third_party_sharing">"لن نشارك بياناتك مع أطراف ثالثة"</string>
<string name="screen_analytics_prompt_title">"ساعدنا على التحسن %1$s"</string>
</resources>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."</string>
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane użytkowania, aby pomóc nam ulepszać aplikację."</string>
<string name="screen_analytics_prompt_read_terms">"Przeczytaj nasze warunki użytkowania %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"مكالمة جارية"</string>
<string name="call_foreground_service_message_android">"اضغط للعودة إلى المكالمة"</string>
<string name="call_foreground_service_title_android">"☎️ الاتصال يتمّ"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"لا يدعم Element Call استخدام أجهزة الصوت عبر Bluetooth في إصدار Android هذا. يرجى اختيار جهاز صوت آخر."</string>
<string name="screen_incoming_call_subtitle_android">"مكالمة Element Call واردة"</string>
</resources>
@@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"تماس خروجی"</string>
<string name="call_foreground_service_message_android">"زدن برای بازگشت به تماس"</string>
<string name="call_foreground_service_title_android">"تماس در جریان ☎️"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"برنامه Element Call در این نسخه اندروید از دستگاه‌های صوتی بلوتوث پشتیبانی نمی‌کند. لطفاً دستگاه صوتی دیگری را انتخاب کنید."</string>
<string name="screen_incoming_call_subtitle_android">"تماس المنتی ورودی"</string>
</resources>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"إنشاء غرفة جديدة"</string>
<string name="screen_create_room_add_people_title">"دعوة أشخاص"</string>
<string name="screen_create_room_error_creating_room">"حصل خطأ أثناء إنشاء الغرفة"</string>
<string name="screen_create_room_error_creating_space">"تعذّر إنشاء المساحة بسبب خطأ غير معروف. حاول مرة أخرى لاحقًا."</string>
<string name="screen_create_room_name_placeholder">"إضافة اسم…"</string>
<string name="screen_create_room_new_room_title">"غرفة جديدة"</string>
<string name="screen_create_room_new_space_title">"مساحة جديدة"</string>
<string name="screen_create_room_private_option_description">"لا يدخل إلا المدعوّون."</string>
<string name="screen_create_room_private_option_title">"خاصة"</string>
<string name="screen_create_room_public_option_description">"بإمكان الجميع العثور على هذه الغرفة.
يمكنك تغيير هذا من إعدادات الغرفة متى شئت."</string>
<string name="screen_create_room_public_option_short_description">"يمكن لأي شخص الانضمام."</string>
<string name="screen_create_room_public_option_title">"عامة"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"يمكن لأي شخص طلب الانضمام، لكن يجب أن يقبل الطلب مسؤول أو مشرف."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"السماح بطلبات الانضمام"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"يمكن لأي شخص في %1$s الانضمام، لكن يجب على الآخرين طلب الوصول."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"طلب الانضمام"</string>
<string name="screen_create_room_room_access_section_private_option_description">"يمكن للمدعوين فقط الانضمام."</string>
<string name="screen_create_room_room_access_section_private_option_title">"خاصة"</string>
<string name="screen_create_room_room_access_section_public_option_description">"يمكن لأي شخص الانضمام."</string>
<string name="screen_create_room_room_access_section_public_option_title">"عامة"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"يمكن لأي شخص في %1$s الانضمام."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"قياسي"</string>
<string name="screen_create_room_room_access_section_title">"من يمكنه الوصول"</string>
<string name="screen_create_room_room_address_section_footer">"تحتاج إلى عنوان حتى تظهر الغرفة في الدليل العام."</string>
<string name="screen_create_room_room_address_section_title">"العنوان"</string>
<string name="screen_create_room_room_visibility_section_title">"ظهور الغرفة"</string>
<string name="screen_create_room_space_selection_no_space_description">"(لا توجد مساحة)"</string>
<string name="screen_create_room_space_selection_no_space_option">"عدم الإضافة إلى مساحة"</string>
<string name="screen_create_room_space_selection_no_space_title">"لم يتم اختيار أي مساحة"</string>
<string name="screen_create_room_space_selection_sheet_title">"إضافة إلى مساحة"</string>
<string name="screen_create_room_topic_label">"الموضوع (اختياري)"</string>
<string name="screen_create_room_topic_placeholder">"إضافة وصف…"</string>
</resources>
@@ -3,14 +3,34 @@
<string name="screen_create_room_action_create_room">"Nueva sala"</string>
<string name="screen_create_room_add_people_title">"Invitar personas"</string>
<string name="screen_create_room_error_creating_room">"Se ha producido un error al crear la sala"</string>
<string name="screen_create_room_error_creating_space">"No se pudo crear el espacio debido a un error desconocido. Inténtalo de nuevo más tarde."</string>
<string name="screen_create_room_name_placeholder">"Añadir nombre…"</string>
<string name="screen_create_room_new_room_title">"Nueva sala"</string>
<string name="screen_create_room_new_space_title">"Nuevo espacio"</string>
<string name="screen_create_room_private_option_description">"Solo las personas invitadas pueden acceder."</string>
<string name="screen_create_room_private_option_title">"Privado"</string>
<string name="screen_create_room_public_option_description">"Cualquiera puede encontrar esta sala.
Puedes cambiar esto en cualquier momento en los ajustes de la sala."</string>
<string name="screen_create_room_public_option_short_description">"Cualquiera puede unirse."</string>
<string name="screen_create_room_public_option_title">"Público"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Cualquiera puede solicitar unirse, pero un administrador o un moderador tendrá que aceptar la solicitud"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Permitir solicitar unirse"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Cualquiera en %1$s puede unirse, pero el resto debe solicitar acceso."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Solicitar unirse"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Solo las personas invitadas pueden unirse."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privado"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Cualquiera puede unirse."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Público"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Cualquiera de %1$s puede unirse."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Estándar"</string>
<string name="screen_create_room_room_access_section_title">"Quién tiene acceso"</string>
<string name="screen_create_room_room_address_section_footer">"Necesitarás una dirección de sala para que sea visible en el directorio de salas públicas."</string>
<string name="screen_create_room_room_address_section_title">"Dirección"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidad de la sala"</string>
<string name="screen_create_room_space_selection_no_space_description">"(ningún espacio)"</string>
<string name="screen_create_room_space_selection_no_space_option">"No añadir a un espacio"</string>
<string name="screen_create_room_space_selection_no_space_title">"No se ha seleccionado ningún espacio"</string>
<string name="screen_create_room_space_selection_sheet_title">"Añadir a un espacio"</string>
<string name="screen_create_room_topic_label">"Tema (opcional)"</string>
<string name="screen_create_room_topic_placeholder">"Añadir descripción…"</string>
</resources>
@@ -3,12 +3,34 @@
<string name="screen_create_room_action_create_room">"اتاق جدید"</string>
<string name="screen_create_room_add_people_title">"دعوت افراد"</string>
<string name="screen_create_room_error_creating_room">"هنگام ایجاد اتاق خطایی رخ داد"</string>
<string name="screen_create_room_error_creating_space">"به دلیل خطای ناشناخته‌ای، فضا ایجاد نشد. بعداً دوباره امتحان کنید."</string>
<string name="screen_create_room_name_placeholder">"افزودن نام…"</string>
<string name="screen_create_room_new_room_title">"اتاق جدید"</string>
<string name="screen_create_room_new_space_title">"فضای جدید"</string>
<string name="screen_create_room_private_option_description">"تنها افراد دعوت شده می‌توانند بپیوندند."</string>
<string name="screen_create_room_private_option_title">"خصوصی"</string>
<string name="screen_create_room_public_option_description">"هرکسی می‌تواند اتاق را بیابد.
می‌توانید بعداً در تظیمات اتاق عوضش کنید."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"درخواست دعوت"</string>
<string name="screen_create_room_room_access_section_public_option_description">"هرکسی می‌تواند به این اتاق بپیوندد"</string>
<string name="screen_create_room_room_address_section_title">"نشانی اتاق"</string>
<string name="screen_create_room_public_option_short_description">"هر کسی می‌تواند عضو شود."</string>
<string name="screen_create_room_public_option_title">"عمومی"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"هر کسی می‌تواند درخواست عضویت بدهد، اما یک مدیر یا ناظر باید درخواست را بپذیرد."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"اجازه درخواست عضویت"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"هر کسی در %1$s می‌تواند عضو شود، اما بقیه باید درخواست دسترسی بدهند."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"درخواست عضویت"</string>
<string name="screen_create_room_room_access_section_private_option_description">"تنها افراد دعوت شده می‌توانند بپیوندند."</string>
<string name="screen_create_room_room_access_section_private_option_title">"خصوصی"</string>
<string name="screen_create_room_room_access_section_public_option_description">"هر کسی می‌تواند عضو شود."</string>
<string name="screen_create_room_room_access_section_public_option_title">"عمومی"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"هر کسی که در %1$s باشد می‌تواند عضو شود."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"استاندارد"</string>
<string name="screen_create_room_room_access_section_title">"چه کسی دسترسی دارد"</string>
<string name="screen_create_room_room_address_section_footer">"برای اینکه بتوانید آن را در فهرست عمومی مشاهده کنید، به یک آدرس نیاز دارید."</string>
<string name="screen_create_room_room_address_section_title">"آدرس"</string>
<string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string>
<string name="screen_create_room_space_selection_no_space_description">"(بدون فضا)"</string>
<string name="screen_create_room_space_selection_no_space_option">"به یک فضا اضافه نکنید"</string>
<string name="screen_create_room_space_selection_no_space_title">"فضایی انتخاب نشده است"</string>
<string name="screen_create_room_space_selection_sheet_title">"به فضا اضافه کنید"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
<string name="screen_create_room_topic_placeholder">"اضافه کردن توضیحات…"</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>
@@ -1,13 +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">"Confirma que quieres desactivar tu cuenta. Esta acción no se puede deshacer."</string>
<string name="screen_deactivate_account_confirmation_dialog_content">"Confirma que quieres eliminar tu cuenta. Esta acción no se puede deshacer."</string>
<string name="screen_deactivate_account_delete_all_messages">"Borrar todos mis mensajes"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Advertencia: Futuros usuarios pueden ver conversaciones incompletas."</string>
<string name="screen_deactivate_account_description">"Desactivar tu cuenta es %1$s:"</string>
<string name="screen_deactivate_account_description">"Eliminar tu cuenta es %1$s:"</string>
<string name="screen_deactivate_account_description_bold_part">"irreversible"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s tu cuenta (no podrás volver a iniciar sesión y tu ID no se podrá volver a utilizar)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Inhabilitará permanentemente"</string>
<string name="screen_deactivate_account_list_item_2">"Te eliminará de todas las salas de chat."</string>
<string name="screen_deactivate_account_list_item_3">"Eliminará la información de tu cuenta de nuestro servidor de identidad."</string>
<string name="screen_deactivate_account_list_item_4">"Tus mensajes seguirán siendo visibles para los usuarios registrados, pero no estarán disponibles para los usuarios nuevos o no registrados si decides eliminarlos."</string>
<string name="screen_deactivate_account_title">"Eliminar cuenta"</string>
</resources>
@@ -1,9 +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_title">"غیرفعّال‌سازی حساب"</string>
<string name="screen_deactivate_account_list_item_4">"پیام‌های شما همچنان برای کاربران ثبت‌نام‌شده قابل مشاهده خواهند بود، اما اگر تصمیم به حذف آنها بگیرید، برای کاربران جدید یا ثبت‌نام‌نشده در دسترس نخواهند بود."</string>
<string name="screen_deactivate_account_title">"حذف حساب کاربری"</string>
</resources>
@@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.Color
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow
/**
@@ -21,9 +20,6 @@ import kotlinx.coroutines.flow.Flow
* On a standard build this reports permissive defaults, so callers do not need to branch on the build type themselves.
*/
interface EnterpriseService {
/** Whether this build is an enterprise one; `false` for the public app. */
val isEnterpriseBuild: Boolean
/**
* Whether the given session belongs to an enterprise deployment, which can be true even on a standard build.
*
@@ -41,17 +37,26 @@ interface EnterpriseService {
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
/**
* The homeservers the user is allowed to sign in to; an empty list, or one containing [ANY_ACCOUNT_PROVIDER], means no restriction.
* Returns the list of homeservers the user is allowed to sign in to.
*
* If the list is empty or contains the special value [ANY_ACCOUNT_PROVIDER], the user is allowed to sign in to any homeserver.
*/
fun defaultHomeserverList(): List<String>
fun homeserverAllowList(): List<String>
/**
* Whether the user is allowed to sign in to a given homeserver, according to [defaultHomeserverList].
* Whether the user is allowed to sign in to a given homeserver, according to [homeserverAllowList].
*
* @param homeserverUrl the server the user is trying to use.
*/
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
/**
* Whether the given homeserver enforces the use of Element Pro or a derived app.
*
* @param homeserverUrl the homeserver to check.
*/
suspend fun isElementProEnforced(homeserverUrl: String): Boolean
/**
* Override the brand color.
* @param sessionId the session to override the brand color for, or null to set the brand color to use when there is no session.
@@ -94,11 +99,6 @@ interface EnterpriseService {
*/
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
/**
* Gets the overridden Element Well-Known data if it has been set, or null if not set.
*/
fun overriddenElementWellKnown(): ElementWellKnown?
/**
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
* Returns `null` when this build does not read its configuration from an ESS deployment.
@@ -113,7 +113,7 @@ interface EnterpriseService {
}
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
return defaultHomeserverList().let {
return homeserverAllowList().let {
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
}
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api
/**
* A use case to check if this build of the app is an enterprise build.
*/
fun interface IsEnterpriseBuild {
/**
* Returns `true` if this build of the app is an enterprise build, `false` otherwise.
*/
operator fun invoke(): Boolean
}
@@ -14,6 +14,9 @@ package io.element.android.features.enterprise.api
* [init] must have run before the other members return meaningful answers.
*/
interface SessionEnterpriseService {
/** Loads the deployment configuration of this session; to be called once when the session starts. */
suspend fun init()
/** Whether Element Call may be used on this session, which a deployment can turn off. */
suspend fun isElementCallAvailable(): Boolean
@@ -26,7 +29,4 @@ interface SessionEnterpriseService {
/** Whether the homeserver forbids creating encrypted rooms, in which case the app must not offer the option. */
suspend fun isEncryptionDisabledByHomeserver(): Boolean
/** Loads the deployment configuration of this session; to be called once when the session starts. */
suspend fun init()
}
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api.remoteconfig
/**
* A provider of a runtime-fetched MapTiler configuration, which can be used to override the default MapTiler configuration.
*/
fun interface CustomMapTilerConfigProvider {
suspend fun get(): Result<MapTilerConfig?>
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api.remoteconfig
import androidx.compose.runtime.Immutable
/**
* A `MapTiler` configuration received from the server. This configuration is used to display maps in the app.
*/
@Immutable
data class MapTilerConfig(
/** The API key to use with MapTiler. This is always not null, but can be empty/blank. */
val apiKey: String,
/** The style ID to use for light mode. This is optional and can be null. */
val lightStyleId: String?,
/** The style ID to use for light mode. This is optional and can be null. */
val darkStyleId: String?,
/** The base URL to use for MapTiler. This is optional and can be null. */
val baseUrl: String?,
)
@@ -1,17 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api.remoteconfig
import io.element.android.libraries.wellknown.api.ElementWellKnown
/**
* Typealias for the well-known configuration used for enterprise features.
*
* This will allow for easier refactoring in the future when we'll want to move as much of its logic as possible to this module.
*/
typealias RemoteEnterpriseConfig = ElementWellKnown
@@ -1,35 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api.remoteconfig
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
/**
* Wrapper to fetch the remote enterprise configuration for a given [SessionId], either from a local cache or from a remote source.
*/
interface RemoteEnterpriseConfigProvider {
/**
* Returns the configuration of a session, served from the cache when possible.
* A stale entry is reported as outdated while a refresh runs in the background, so this rarely waits on the network.
*
* @param sessionId the session whose configuration is requested.
*/
suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig>
/**
* Creates a provider, which needs a fetcher because the configuration is read over HTTP.
*/
interface Factory {
/**
* @param urlContentFetcher used to perform the request; a client that is not authenticated yet also works.
*/
fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api.remoteconfig
sealed interface RemoteEnterpriseConfigResult<out T> {
/**
* Well-known data has been successfully retrieved.
*/
data class Success<out T>(val data: T) : RemoteEnterpriseConfigResult<T>
/**
* Well-known data has been retrieved from the local cache but is outdated and a manual refresh has been launched.
*/
data class Outdated<out T>(val data: T) : RemoteEnterpriseConfigResult<T>
/**
* Well-known data is not found (file does not exist server side, we got a 404).
*/
data object NotFound : RemoteEnterpriseConfigResult<Nothing>
/**
* Any other error.
*/
data class Error(val exception: Exception) : RemoteEnterpriseConfigResult<Nothing>
fun dataOrNull(): T? = when (this) {
is Success<T> -> data
is Outdated<T> -> data
is Error -> null
NotFound -> null
}
fun upToDateDataOrNull(): T? = when (this) {
is Success<T> -> data
is Outdated<T> -> null
is Error -> null
NotFound -> null
}
}
@@ -16,18 +16,16 @@ import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@ContributesBinding(AppScope::class)
class DefaultEnterpriseService : EnterpriseService {
override val isEnterpriseBuild = false
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher) = url
override fun defaultHomeserverList(): List<String> = emptyList()
override fun homeserverAllowList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = false
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
@@ -48,7 +46,5 @@ class DefaultEnterpriseService : EnterpriseService {
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
override fun overriddenElementWellKnown(): ElementWellKnown? = null
override fun essConfigEndpointUrl(domain: String): String? = null
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.IsEnterpriseBuild
@ContributesBinding(AppScope::class)
class DefaultIsEnterpriseBuild : IsEnterpriseBuild {
override fun invoke(): Boolean = false
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.impl.remoteconfig
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.libraries.di.SessionScope
@ContributesBinding(SessionScope::class)
class DefaultCustomMapTilerConfigProvider : CustomMapTilerConfigProvider {
override suspend fun get(): Result<MapTilerConfig?> = Result.success(null)
}
@@ -1,30 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.impl.remoteconfig
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
@ContributesBinding(AppScope::class)
class DefaultRemoteEnterpriseConfigProvider : RemoteEnterpriseConfigProvider {
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
return WellknownRetrieverResult.NotFound
}
@ContributesBinding(AppScope::class)
class Factory : RemoteEnterpriseConfigProvider.Factory {
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider {
return DefaultRemoteEnterpriseConfigProvider()
}
}
}
@@ -19,15 +19,9 @@ import org.junit.Test
class DefaultEnterpriseServiceTest {
@Test
fun `isEnterpriseBuild is false`() {
fun homeserverWhitelist() {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse()
}
@Test
fun `defaultHomeserverList should return empty list`() {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty()
assertThat(defaultEnterpriseService.homeserverAllowList()).isEmpty()
}
@Test
@@ -14,7 +14,6 @@ import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
@@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeEnterpriseService(
override val isEnterpriseBuild: Boolean = false,
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
@@ -33,8 +31,8 @@ class FakeEnterpriseService(
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
private val tweakMasUrlResult: (String, String, UrlContentFetcher) -> String = { _, _, _ -> lambdaError() },
private val overrideWellKnownResult: () -> ElementWellKnown? = { lambdaError() },
private val essConfigEndpointUrlResult: (String) -> String = { lambdaError() },
private val isElementProEnforcedResult: (String) -> Boolean = { lambdaError() },
) : EnterpriseService {
private val brandColorState = MutableStateFlow(initialBrandColor)
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
@@ -47,7 +45,7 @@ class FakeEnterpriseService(
tweakMasUrlResult(url, homeserver, urlContentFetcher)
}
override fun defaultHomeserverList(): List<String> {
override fun homeserverAllowList(): List<String> {
return defaultHomeserverListResult()
}
@@ -55,6 +53,10 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl)
}
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = simulateLongTask {
isElementProEnforcedResult(homeserverUrl)
}
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
overrideBrandColorResult(sessionId, brandColor)
}
@@ -84,10 +86,6 @@ class FakeEnterpriseService(
return getNoisyNotificationChannelIdResult(sessionId)
}
override fun overriddenElementWellKnown(): ElementWellKnown? {
return overrideWellKnownResult()
}
override fun essConfigEndpointUrl(domain: String): String {
return essConfigEndpointUrlResult(domain)
}
@@ -1,21 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.test.remoteconfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
class FakeRemoteEnterpriseConfigProvider(
private val getResult: (SessionId) -> WellknownRetrieverResult<RemoteEnterpriseConfig> = { WellknownRetrieverResult.NotFound }
) : RemoteEnterpriseConfigProvider {
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
return getResult(sessionId)
}
}
@@ -1,17 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.test.remoteconfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.libraries.matrix.api.UrlContentFetcher
class FakeRemoteEnterpriseConfigProviderFactory(
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
) : RemoteEnterpriseConfigProvider.Factory {
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.test.remoteconfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
fun aMapTilerConfig(
apiKey: String = "test_api_key",
baseUrl: String? = null,
lightStyleId: String? = null,
darkStyleId: String? = null,
) = MapTilerConfig(
apiKey = apiKey,
baseUrl = baseUrl,
lightStyleId = lightStyleId,
darkStyleId = darkStyleId,
)
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"لا يمكنك التأكيد؟"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"إنشاء مفتاح استرداد جديد"</string>
<string name="screen_identity_confirmation_subtitle">"اختر طريقة التحقق لإعداد المراسلة الآمنة."</string>
<string name="screen_identity_confirmation_title">"تأكيد هويتك الرقمية"</string>
<string name="screen_identity_confirmation_use_another_device">"استخدام جهاز آخر"</string>
<string name="screen_identity_confirmation_use_recovery_key">"استخدام مفتاح الاسترداد"</string>
<string name="screen_identity_confirmed_subtitle">"يمكنك الآن قراءة الرسائل أو إرسالها بأمان، ويمكن لأي شخص تدردش معه أن يثق بهذا الجهاز أيضًا."</string>
<string name="screen_identity_confirmed_title">"تم التحقق من الجهاز"</string>
<string name="screen_identity_use_another_device">"استخدام جهاز آخر"</string>
<string name="screen_identity_waiting_on_other_device">"في انتظار الجهاز الآخر…"</string>
<string name="screen_notification_optin_subtitle">"يمكنك تغيير إعداداتك لاحقاً."</string>
<string name="screen_notification_optin_title">"السماح بالإشعارات وعدم تفويت أي رسالة"</string>
</resources>
@@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"¿No puedes confirmar?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Crear una nueva clave de recuperación"</string>
<string name="screen_identity_confirmation_subtitle">"Verifica este dispositivo para configurar la mensajería segura."</string>
<string name="screen_identity_confirmation_title">"Confirma que eres tú"</string>
<string name="screen_identity_confirmation_subtitle">"Elige cómo verificar para configurar la mensajería segura."</string>
<string name="screen_identity_confirmation_title">"Confirma tu identidad digital"</string>
<string name="screen_identity_confirmation_use_another_device">"Usar otro dispositivo"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Usar clave de recuperación"</string>
<string name="screen_identity_confirmed_subtitle">"Ahora puedes leer o enviar mensajes de forma segura y cualquier persona con la que chatees también puede confiar en este dispositivo."</string>
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"عطّل تحسين البطارية لهذا التطبيق للتأكد من استلام جميع الإشعارات."</string>
<string name="banner_battery_optimization_submit_android">"تعطيل التحسين"</string>
<string name="banner_battery_optimization_title_android">"هل لا تصلك الإشعارات؟"</string>
<string name="banner_new_sound_message">"تم تحديث صوت إشعاراتك، ليصبح أوضح وأسرع وأقل إزعاجًا."</string>
<string name="banner_new_sound_title">"حدّثنا الأصوات لديك"</string>
<string name="banner_set_up_recovery_content">"يتم نسخ دردشاتك احتياطيًا تلقائيًا بتشفير من طرف إلى طرف. لاستعادة هذه النسخة الاحتياطية والاحتفاظ بهويتك الرقمية إذا فقدت الوصول إلى جميع أجهزتك، ستحتاج إلى مفتاح الاسترداد."</string>
<string name="banner_set_up_recovery_submit">"الحصول على مفتاح الاسترداد"</string>
<string name="banner_set_up_recovery_title">"احتفِظ بنسخة احتياطية من دردشاتك"</string>
<string name="confirm_recovery_key_banner_message">"أكّد مفتاح الاسترداد الخاص بك للحفاظ على الوصول إلى تخزين المفاتيح وسجل الرسائل."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"أدخل مفتاح الاسترداد"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"هل نسيت مفتاح الاسترداد؟"</string>
<string name="confirm_recovery_key_banner_title">"تخزين المفاتيح لديك غير متزامن"</string>
<string name="full_screen_intent_banner_message">"للتأكد من عدم تفويت أي مكالمة مهمة، يُرجى تغيير إعداداتك للسماح بإشعارات ملء الشاشة عندما يكون هاتفك مقفلاً."</string>
<string name="full_screen_intent_banner_title">"حسّن تجربة مكالماتك"</string>
<string name="screen_home_tab_chats">"المحادثات"</string>
<string name="screen_home_tab_spaces">"المساحات"</string>
<string name="screen_invites_decline_chat_message">"أمتأكد من رغبتك برفض دعوة للانضمام إلى %1$s؟"</string>
<string name="screen_invites_decline_chat_title">"رفض الدعوة"</string>
<string name="screen_invites_decline_direct_chat_message">"أمتأكد من رغبتك برفض المحادثة الخاصة مع %1$s؟"</string>
<string name="screen_invites_decline_direct_chat_title">"رفض المحادثة"</string>
<string name="screen_invites_empty_list">"لا دعوات"</string>
<string name="screen_invites_invited_you">"دعاك %1$s (%2$s)"</string>
<string name="screen_migration_message">"هذه العملية تُجرى لمرة واحدة، شكراً على انتظارك."</string>
<string name="screen_migration_title">"إعداد حسابك"</string>
<string name="screen_roomlist_a11y_create_message">"إنشاء محادثة جديد أو غرفة"</string>
<string name="screen_roomlist_clear_filters">"مسح عوامل التصفية"</string>
<string name="screen_roomlist_empty_message">"ابدأ الآن بمراسلة من تشاء."</string>
<string name="screen_roomlist_empty_title">"لا محادثات بعد."</string>
<string name="screen_roomlist_filter_favourites">"المفضلة"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"يمكنك إضافة محادثة إلى المفضلة من إعدادات المحادثة.
يمكنك الآن إلغاء تحديد عوامل التصفية لرؤية محادثاتك الأخرى"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"ليست لديك أي محادثات مفضلة بعد"</string>
<string name="screen_roomlist_filter_invites">"الدعوات"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"ليس لديك أي دعوات قيد الانتظار."</string>
<string name="screen_roomlist_filter_low_priority">"منخفضة الأولوية"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"ليست لديك أي دردشات منخفضة الأولوية بعد"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"يمكنك إلغاء تحديد عوامل التصفية لرؤية محادثاتك الأخرى"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"ليست لديك محادثات لهذا الاختيار"</string>
<string name="screen_roomlist_filter_people">"الأشخاص"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"ليست لديك أي رسائل مباشرة بعد"</string>
<string name="screen_roomlist_filter_rooms">"الغرف"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"لست في أي غرفة بعد"</string>
<string name="screen_roomlist_filter_unreads">"غير المقروءة"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"تهانينا!
ليست لديك أي رسائل غير مقروءة!"</string>
<string name="screen_roomlist_knock_event_sent_description">"تم إرسال طلب الانضمام"</string>
<string name="screen_roomlist_main_space_title">"المحادثات"</string>
<string name="screen_roomlist_mark_as_read">"وضع علامة كمقروء"</string>
<string name="screen_roomlist_mark_as_unread">"وضع علامة كغير مقروء"</string>
<string name="screen_roomlist_tombstoned_room_description">"تمت ترقية هذه الغرفة"</string>
<string name="screen_roomlist_your_spaces">"مساحاتك"</string>
<string name="session_verification_banner_message">"يبدو أنك تستخدم جهازاً جديداً. تحقق باستخدام جهاز آخر ليصبح لك وصول إلى رسائلك المشفرة."</string>
<string name="session_verification_banner_title">"أثبت هويتك"</string>
</resources>
@@ -3,9 +3,11 @@
<string name="banner_battery_optimization_content_android">"Deshabilita la optimización de la batería en esta aplicación para asegurarte de que recibes notificaciones."</string>
<string name="banner_battery_optimization_submit_android">"Deshabilitar la optimización"</string>
<string name="banner_battery_optimization_title_android">"¿No llegan las notificaciones?"</string>
<string name="banner_set_up_recovery_content">"Recupera tu identidad criptográfica y tu historial de mensajes con una clave de recuperación si has perdido todos tus dispositivos actuales."</string>
<string name="banner_set_up_recovery_submit">"Configurar la recuperación"</string>
<string name="banner_set_up_recovery_title">"Configura la recuperación para proteger tu cuenta"</string>
<string name="banner_new_sound_message">"Se ha actualizado tu sonido de notificación: más claro, rápido y menos disruptivo."</string>
<string name="banner_new_sound_title">"Hemos actualizado tus sonidos"</string>
<string name="banner_set_up_recovery_content">"Tus chats se respaldan automáticamente con cifrado de extremo a extremo. Para restaurar esta copia de seguridad y conservar tu identidad digital si pierdes el acceso a todos tus dispositivos, necesitarás tu clave de recuperación."</string>
<string name="banner_set_up_recovery_submit">"Obtener clave de recuperación"</string>
<string name="banner_set_up_recovery_title">"Copia de seguridad de tus chats"</string>
<string name="confirm_recovery_key_banner_message">"Confirma tu clave de recuperación para mantener el acceso a tu almacén de claves y al historial de mensajes."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Introduce tu clave de recuperación"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"¿Olvidaste tu clave de recuperación?"</string>
@@ -13,6 +15,7 @@
<string name="full_screen_intent_banner_message">"Para asegurarte de que nunca te pierdas una llamada importante, modifica tus ajustes para permitir notificaciones a pantalla completa cuando el teléfono esté bloqueado."</string>
<string name="full_screen_intent_banner_title">"Mejora tu experiencia de llamada"</string>
<string name="screen_home_tab_chats">"Chats"</string>
<string name="screen_home_tab_spaces">"Espacios"</string>
<string name="screen_invites_decline_chat_message">"¿Estás seguro de que quieres rechazar la invitación a unirte a %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Rechazar la invitación"</string>
<string name="screen_invites_decline_direct_chat_message">"¿Estás seguro de que quieres rechazar este chat privado con %1$s?"</string>
@@ -32,6 +35,7 @@ Por ahora, puedes deseleccionar los filtros para ver tus otros chats"</string>
<string name="screen_roomlist_filter_invites">"Invitaciones"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"No tienes ninguna invitación pendiente."</string>
<string name="screen_roomlist_filter_low_priority">"Prioridad baja"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Aún no tienes ningún chat de prioridad baja"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Puedes deseleccionar filtros para ver tus otros chats."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"No tienes chats para esta selección"</string>
<string name="screen_roomlist_filter_people">"Personas"</string>
@@ -46,6 +50,7 @@ Por ahora, puedes deseleccionar los filtros para ver tus otros chats"</string>
<string name="screen_roomlist_mark_as_read">"Marcar como leído"</string>
<string name="screen_roomlist_mark_as_unread">"Marcar como no leído"</string>
<string name="screen_roomlist_tombstoned_room_description">"Esta sala ha sido renovada"</string>
<string name="screen_roomlist_your_spaces">"Tus espacios"</string>
<string name="session_verification_banner_message">"Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados."</string>
<string name="session_verification_banner_title">"Verifica que eres tú"</string>
</resources>
@@ -3,12 +3,16 @@
<string name="banner_battery_optimization_content_android">"از کار انداختن بهینه‌سازی باتری برای این کاره برای اطمینان از گرفتن همهٔ آگاهی‌ها."</string>
<string name="banner_battery_optimization_submit_android">"از کار انداختن بهینه سازی"</string>
<string name="banner_battery_optimization_title_android">"آگاهی‌ها نمی‌رسند؟"</string>
<string name="banner_set_up_recovery_content">"بازگردانی تاریخچهٔ پیام‌ها و هویت رمزنگاشته‌تان با کلید بازیابی در صورت از دست دادن همهٔ افزاره‌های موجودتان."</string>
<string name="banner_new_sound_message">"پینگ اعلان شما به‌روزرسانی شده است—واضح‌تر، سریع‌تر و کم‌دردسرتر."</string>
<string name="banner_new_sound_title">"ما صداهای شما را تازه کرده‌ایم"</string>
<string name="banner_set_up_recovery_content">"چت‌های شما به طور خودکار با رمزگذاری سراسری پشتیبان‌گیری می‌شوند. برای بازیابی این پشتیبان‌گیری و حفظ هویت دیجیتال خود در زمانی که دسترسی به تمام دستگاه‌هایتان را از دست می‌دهید، به کلید بازیابی خود نیاز خواهید داشت."</string>
<string name="banner_set_up_recovery_submit">"دریافت کلید بازیابی"</string>
<string name="banner_set_up_recovery_title">"برپایی بازیابی"</string>
<string name="banner_set_up_recovery_title">"از چت‌هایتان پشتیبان بگیرید"</string>
<string name="confirm_recovery_key_banner_message">"کلید بازیابی خود را تأیید کنید تا دسترسی به حافظه کلیدها و تاریخچه پیام‌هایتان حفظ شود ."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"ورود کلید بازیابیتان"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"کلید بازیابی خود را فراموش کرده‌اید؟"</string>
<string name="confirm_recovery_key_banner_title">"ذخیره‌ساز کلیدتان از هم‌گام بودن در آمده"</string>
<string name="full_screen_intent_banner_message">"برای اطمینان از اینکه هرگز تماس مهمی را از دست نمی‌دهید، لطفاً تنظیمات خود را تغییر دهید تا وقتی تلفنتان قفل است، اعلان‌ها به صورت تمام صفحه نمایش داده شوند."</string>
<string name="full_screen_intent_banner_title">"بهبود تجریهٔ تماستان"</string>
<string name="screen_home_tab_chats">"گپ‌ها"</string>
<string name="screen_home_tab_spaces">"فضاها"</string>
@@ -30,6 +34,7 @@
<string name="screen_roomlist_filter_invites">"دعوت‌ها"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"هیچ دعوت منتظری ندارید."</string>
<string name="screen_roomlist_filter_low_priority">"اولویت کم"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"شما هنوز هیچ چت با اولویت پایین ندارید"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"می توانید پالایه‌ها را برای دیدن دیگر گپ‌هایتان بردارید"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"هیچ گپی برای این گزینش ندارید"</string>
<string name="screen_roomlist_filter_people">"افراد"</string>
@@ -44,6 +49,7 @@
<string name="screen_roomlist_mark_as_read">"علامت‌گذاری به عنوان خوانده شده"</string>
<string name="screen_roomlist_mark_as_unread">"نشان به ناخوانده"</string>
<string name="screen_roomlist_tombstoned_room_description">"این اتاق ارتقا یافته"</string>
<string name="screen_roomlist_your_spaces">"فضاهای شما"</string>
<string name="session_verification_banner_message">"گویا از افزاره‌ای جدید استفاده می‌کنید. تأیید با افزاره‌ای دیگر برای دسترسی به پیام‌های رمزنگاری شده‌تان."</string>
<string name="session_verification_banner_title">"تأیید کنید که خودتانید"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"لن ترى أي رسائل أو دعوات إلى غرف من هذا المستخدم"</string>
<string name="screen_decline_and_block_block_user_option_title">"حظر المستخدم"</string>
<string name="screen_decline_and_block_report_user_option_description">"أبلغ موفّر حسابك عن هذه الغرفة."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"صِف سبب الإبلاغ…"</string>
<string name="screen_decline_and_block_title">"رفض وحظر"</string>
<string name="screen_invites_decline_chat_message">"أمتأكد من رغبتك برفض دعوة للانضمام إلى %1$s؟"</string>
<string name="screen_invites_decline_chat_title">"رفض الدعوة"</string>
<string name="screen_invites_decline_direct_chat_message">"أمتأكد من رغبتك برفض المحادثة الخاصة مع %1$s؟"</string>
<string name="screen_invites_decline_direct_chat_title">"رفض المحادثة"</string>
<string name="screen_invites_empty_list">"لا دعوات"</string>
<string name="screen_invites_invited_you">"دعاك %1$s (%2$s)"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"نعم، ارفض واحظر"</string>
<string name="screen_join_room_decline_and_block_alert_message">"هل أنت متأكد من رغبتك في رفض الدعوة للانضمام إلى هذه الغرفة؟ سيمنع هذا أيضًا %1$s من التواصل معك أو دعوتك إلى غرف."</string>
<string name="screen_join_room_decline_and_block_alert_title">"رفض الدعوة والحظر"</string>
<string name="screen_join_room_decline_and_block_button_title">"رفض وحظر"</string>
</resources>
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"شما هیچ پیام یا دعوتنامه‌ای از این کاربر نخواهید دید."</string>
<string name="screen_decline_and_block_block_user_option_title">"انسداد کاربر"</string>
<string name="screen_decline_and_block_report_user_option_description">"این اتاق را به ارائه دهنده حساب خود گزارش دهید."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"شرح دلیل گزارش…"</string>
<string name="screen_decline_and_block_title">"رد و انسداد"</string>
<string name="screen_invites_decline_chat_message">"مطمئنید که می‌خواهید دعوت پیوستن به %1$s را رد کنید؟"</string>
@@ -10,6 +12,7 @@
<string name="screen_invites_empty_list">"بدون دعوت"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) دعوتتان کرد"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"بله. رد و انسداد"</string>
<string name="screen_join_room_decline_and_block_alert_message">"آیا مطمئنید که می‌خواهید دعوت به عضویت در این اتاق را رد کنید؟ این کار همچنین مانع از تماس %1$s با شما یا دعوت شما به اتاق‌ها می‌شود."</string>
<string name="screen_join_room_decline_and_block_alert_title">"رد دعوت و انسداد"</string>
<string name="screen_join_room_decline_and_block_button_title">"رد و انسداد"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"أنت عضو بالفعل"</string>
<string name="screen_invite_users_already_invited">"مَدعُوّ بالفعل"</string>
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"ليست لديك حاليًا أي دردشات مع جهات الاتصال هذه. أكّد دعوتهم إلى هذه الغرفة قبل المتابعة."</string>
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"ليست لديك حاليًا أي دردشات مع جهة الاتصال هذه. أكّد دعوتها إلى هذه الغرفة قبل المتابعة."</string>
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"هل تريد دعوة جهات اتصال جديدة إلى هذه الغرفة؟"</string>
<string name="screen_invite_users_confirm_dialog_title_one_user">"هل تريد دعوة جهة اتصال جديدة إلى هذه الغرفة؟"</string>
</resources>
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Ya eres miembro"</string>
<string name="screen_invite_users_already_a_member">"Ya es miembro"</string>
<string name="screen_invite_users_already_invited">"Ya estás invitado"</string>
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"De momento no tienes ningún chat con estos contactos. Confirma que quieres invitarlos a esta sala antes de continuar."</string>
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"De momento no tienes ningún chat con este contacto. Confirma que quieres invitarlo a esta sala antes de continuar."</string>
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"¿Invitar contactos nuevos a esta sala?"</string>
<string name="screen_invite_users_confirm_dialog_title_one_user">"¿Invitar contacto nuevo a esta sala?"</string>
</resources>
@@ -2,4 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"از پیش عضو است"</string>
<string name="screen_invite_users_already_invited">"از پیش دعوت شده"</string>
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"شما در حال حاضر هیچ گفت‌وگویی با این مخاطبین ندارید. قبل از ادامه، دعوت آنها را به این اتاق تأیید کنید."</string>
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"شما در حال حاضر هیچ گفت‌وگویی با این مخاطب ندارید. قبل از ادامه، دعوت او را به این اتاق تأیید کنید."</string>
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"مخاطبین جدید را به این اتاق دعوت کنید؟"</string>
<string name="screen_invite_users_confirm_dialog_title_one_user">"مخاطب جدید را به این اتاق دعوت می‌کنید؟"</string>
</resources>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"لقد حظرك %1$s."</string>
<string name="screen_join_room_ban_message">"تم حظرك"</string>
<string name="screen_join_room_ban_reason">"السبب: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"إلغاء الطلب"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"نعم، ألغِ"</string>
<string name="screen_join_room_cancel_knock_alert_description">"هل أنت متأكد من رغبتك في إلغاء طلبك للانضمام إلى هذه الغرفة؟"</string>
<string name="screen_join_room_cancel_knock_alert_title">"إلغاء طلب الانضمام"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"نعم، ارفض واحظر"</string>
<string name="screen_join_room_decline_and_block_alert_message">"هل أنت متأكد من رغبتك في رفض الدعوة للانضمام إلى هذه الغرفة؟ سيمنع هذا أيضًا %1$s من التواصل معك أو دعوتك إلى غرف."</string>
<string name="screen_join_room_decline_and_block_alert_title">"رفض الدعوة والحظر"</string>
<string name="screen_join_room_decline_and_block_button_title">"رفض وحظر"</string>
<string name="screen_join_room_fail_message">"فشل الانضمام"</string>
<string name="screen_join_room_fail_reason">"إما أنك تحتاج إلى دعوة للانضمام، أو قد توجد قيود على الوصول."</string>
<string name="screen_join_room_forget_action">"نسيان"</string>
<string name="screen_join_room_invite_required_message">"تحتاج إلى دعوة للانضمام"</string>
<string name="screen_join_room_invited_by">"بدعوة من"</string>
<string name="screen_join_room_join_action">"إنضمام"</string>
<string name="screen_join_room_join_restricted_message">"قد تحتاج إلى دعوة أو إلى أن تكون عضوًا في مساحة لتتمكن من الانضمام."</string>
<string name="screen_join_room_knock_action">"إرسال طلب الانضمام"</string>
<string name="screen_join_room_knock_message_characters_count">"الأحرف المسموح بها: %1$d من %2$d"</string>
<string name="screen_join_room_knock_message_description">"رسالة (اختيارية)"</string>
<string name="screen_join_room_knock_sent_description">"ستتلقى دعوة للانضمام إلى الغرفة إذا تم قبول طلبك."</string>
<string name="screen_join_room_knock_sent_title">"تم إرسال طلب الانضمام"</string>
<string name="screen_join_room_loading_alert_message">"تعذّر عرض معاينة الغرفة. قد يكون ذلك بسبب مشكلات في الشبكة أو الخادم."</string>
<string name="screen_join_room_loading_alert_title">"تعذّر عرض معاينة هذه الغرفة"</string>
<string name="screen_join_room_space_not_supported_description">"لا يدعم %1$s المساحات بعد. يمكنك الوصول إلى المساحات عبر الويب."</string>
<string name="screen_join_room_space_not_supported_title">"المساحات غير مدعومة بعد"</string>
<string name="screen_join_room_subtitle_knock">"انقر على الزر أدناه وسيتم إشعار مسؤول الغرفة. ستتمكن من الانضمام إلى المحادثة بعد الموافقة."</string>
<string name="screen_join_room_subtitle_no_preview">"يجب أن تكون عضوًا في هذه الغرفة لعرض سجل الرسائل."</string>
<string name="screen_join_room_title_knock">"هل تريد الانضمام إلى هذه الغرفة؟"</string>
<string name="screen_join_room_title_no_preview">"المعاينة غير متاحة"</string>
</resources>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"Has sido baneado por %1$s."</string>
<string name="screen_join_room_ban_message">"Has sido baneado."</string>
<string name="screen_join_room_ban_by_message">"Has sido vetado por %1$s."</string>
<string name="screen_join_room_ban_message">"Has sido vetado"</string>
<string name="screen_join_room_ban_reason">"Motivo: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"Cancelar solicitud"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Sí, cancelar"</string>
@@ -15,9 +15,11 @@
<string name="screen_join_room_fail_reason">"O es necesario estar invitado para unirse o puede haber restricciones de acceso."</string>
<string name="screen_join_room_forget_action">"Olvidar"</string>
<string name="screen_join_room_invite_required_message">"Necesitas una invitación para unirte"</string>
<string name="screen_join_room_invited_by">"Te invitó"</string>
<string name="screen_join_room_join_action">"Unirse"</string>
<string name="screen_join_room_join_restricted_message">"Es posible que necesites ser invitado o ser miembro de un espacio para poder unirte."</string>
<string name="screen_join_room_knock_action">"Enviar solicitud de unión"</string>
<string name="screen_join_room_knock_message_characters_count">"%1$d de %2$d caracteres permitidos"</string>
<string name="screen_join_room_knock_message_description">"Mensaje (opcional)"</string>
<string name="screen_join_room_knock_sent_description">"Recibirás una invitación para unirte a la sala si se acepta tu solicitud."</string>
<string name="screen_join_room_knock_sent_title">"Solicitud de unión enviada"</string>
@@ -1,23 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"به دست %1$s از این اتاق محروم شدید."</string>
<string name="screen_join_room_ban_message">"از این اتاق محروم شدید"</string>
<string name="screen_join_room_ban_by_message">"شما توسط %1$s مسدود شده‌اید."</string>
<string name="screen_join_room_ban_message">"شما محروم شدید"</string>
<string name="screen_join_room_ban_reason">"دلیل: %1$s."</string>
<string name="screen_join_room_cancel_knock_action">"لغو درخواست"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"بله. لغو"</string>
<string name="screen_join_room_cancel_knock_alert_description">"آیا مطمئن هستید که می‌خواهید درخواست عضویت خود در این اتاق را لغو کنید؟"</string>
<string name="screen_join_room_cancel_knock_alert_title">"لغو درخواست پیوستن"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"بله. رد و انسداد"</string>
<string name="screen_join_room_decline_and_block_alert_message">"آیا مطمئنید که می‌خواهید دعوت به عضویت در این اتاق را رد کنید؟ این کار همچنین مانع از تماس %1$s با شما یا دعوت شما به اتاق‌ها می‌شود."</string>
<string name="screen_join_room_decline_and_block_alert_title">"رد دعوت و انسداد"</string>
<string name="screen_join_room_decline_and_block_button_title">"رد و انسداد"</string>
<string name="screen_join_room_fail_message">"پیوستن شکست خورد"</string>
<string name="screen_join_room_forget_action">"فراموشی این اتاق"</string>
<string name="screen_join_room_invite_required_message">"برای پیوستن به این اتاق نیاز به دعوت دارید"</string>
<string name="screen_join_room_fail_reason">"یا باید دعوتنامه داشته باشید یا ممکن است محدودیت‌هایی برای دسترسی وجود داشته باشد."</string>
<string name="screen_join_room_forget_action">"فراموش کردن"</string>
<string name="screen_join_room_invite_required_message">"برای عضویت نیاز به دعوتنامه دارید"</string>
<string name="screen_join_room_invited_by">"دعوت شده از سوی"</string>
<string name="screen_join_room_join_action">"پیوستن"</string>
<string name="screen_join_room_join_restricted_message">"برای پیوستن به فضا باید دعوت شده باشید."</string>
<string name="screen_join_room_knock_action">"در زدن برای پیوستن"</string>
<string name="screen_join_room_knock_message_characters_count">"کاراکترهای مجاز %1$d از %2$d"</string>
<string name="screen_join_room_knock_message_description">"پیام (اختیاری)"</string>
<string name="screen_join_room_knock_sent_description">"در صورت پذیرش درخواست، دعوتنامه‌ای برای ورود به اتاق دریافت خواهید کرد."</string>
<string name="screen_join_room_knock_sent_title">"درخواست پیوستن فرستاده شد"</string>
<string name="screen_join_room_loading_alert_message">"ما نتوانستیم پیش‌نمایش اتاق را نمایش دهیم. این ممکن است به دلیل مشکلات شبکه یا سرور باشد."</string>
<string name="screen_join_room_loading_alert_title">"ما نتوانستیم پیش‌نمایش این اتاق را نمایش دهیم"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s هنوز از فضاها پشتیبانی نمی‌کند. می‌توانید روی وب به فضاها دسترسی داشته باشید."</string>
<string name="screen_join_room_space_not_supported_title">"فضاها هنوز پشتیبانی نمی‌شوند"</string>
<string name="screen_join_room_subtitle_knock">"زدن روی این دکمه برای آگاه شدن مدیر اتاق. پس از تأیید می‌توانید به گفت‌وگو بپیوندید."</string>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"نعم، قبول الكل"</string>
<string name="screen_knock_requests_list_accept_all_alert_description">"هل أنت متأكد من رغبتك في قبول جميع طلبات الانضمام؟"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"قبول جميع الطلبات"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"قبول الكل"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"تعذّر قبول جميع الطلبات. هل ترغب في المحاولة مرة أخرى؟"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"فشل قبول جميع الطلبات"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"جارٍ قبول جميع طلبات الانضمام"</string>
<string name="screen_knock_requests_list_accept_failed_alert_description">"تعذّر قبول هذا الطلب. هل ترغب في المحاولة مرة أخرى؟"</string>
<string name="screen_knock_requests_list_accept_failed_alert_title">"فشل قبول الطلب"</string>
<string name="screen_knock_requests_list_accept_loading_title">"جارٍ قبول طلب الانضمام"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"نعم، ارفض واحظر"</string>
<string name="screen_knock_requests_list_ban_alert_description">"هل أنت متأكد من رغبتك في رفض %1$s وحظره؟ لن يتمكن هذا المستخدم من طلب الوصول للانضمام إلى هذه الغرفة مرة أخرى."</string>
<string name="screen_knock_requests_list_ban_alert_title">"رفض الوصول والحظر"</string>
<string name="screen_knock_requests_list_ban_loading_title">"جارٍ رفض الوصول وحظره"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"نعم، ارفض"</string>
<string name="screen_knock_requests_list_decline_alert_description">"هل أنت متأكد من رغبتك في رفض طلب %1$s للانضمام إلى هذه الغرفة؟"</string>
<string name="screen_knock_requests_list_decline_alert_title">"رفض الوصول"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"رفض وحظر"</string>
<string name="screen_knock_requests_list_decline_failed_alert_description">"تعذّر رفض هذا الطلب. هل ترغب في المحاولة مرة أخرى؟"</string>
<string name="screen_knock_requests_list_decline_failed_alert_title">"فشل رفض الطلب"</string>
<string name="screen_knock_requests_list_decline_loading_title">"جارٍ رفض طلب الانضمام"</string>
<string name="screen_knock_requests_list_empty_state_description">"عندما يطلب أحدهم الانضمام إلى الغرفة، ستتمكن من رؤية طلبه هنا."</string>
<string name="screen_knock_requests_list_empty_state_title">"لا توجد طلبات انضمام معلّقة"</string>
<string name="screen_knock_requests_list_initial_loading_title">"جارٍ تحميل طلبات الانضمام…"</string>
<string name="screen_knock_requests_list_title">"طلبات الانضمام"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="zero">"%1$s يريد الانضمام إلى هذه الغرفة"</item>
<item quantity="one">"%1$s +%2$d آخر يريدان الانضمام إلى هذه الغرفة"</item>
<item quantity="two">"%1$s +%2$d آخران يريدون الانضمام إلى هذه الغرفة"</item>
<item quantity="few">"%1$s +%2$d آخرون يريدون الانضمام إلى هذه الغرفة"</item>
<item quantity="many">"%1$s +%2$d آخرين يريدون الانضمام إلى هذه الغرفة"</item>
<item quantity="other">"%1$s +%2$d آخرين يريدون الانضمام إلى هذه الغرفة"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"عرض الكل"</string>
<string name="screen_room_single_knock_request_accept_button_title">"قبول"</string>
<string name="screen_room_single_knock_request_title">"%1$s يريد الانضمام إلى هذه الغرفة"</string>
<string name="screen_room_single_knock_request_view_button_title">"عرض"</string>
</resources>
@@ -1,21 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"بله. پذیرش همه"</string>
<string name="screen_knock_requests_list_accept_all_alert_description">"آیا مطمئن هستید که می‌خواهید همه درخواست‌های عضویت را بپذیرید؟"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"پذیرش همهٔ درخواست‌ها"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"پذیرش همه"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"ما نتوانستیم همه درخواست‌ها را بپذیریم. آیا مایلید دوباره امتحان کنید؟"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"شکست در پذیرش همهٔ درخواست‌ها"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"پذیرفتن همهٔ درخواست‌های پیوستن"</string>
<string name="screen_knock_requests_list_accept_failed_alert_description">"ما نتوانستیم این درخواست را بپذیریم. آیا مایلید دوباره امتحان کنید؟"</string>
<string name="screen_knock_requests_list_accept_failed_alert_title">"شکست در پذیرش درخواست"</string>
<string name="screen_knock_requests_list_accept_loading_title">"پذیرفتن درخواست پیوستن"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"بله. رد و انسداد"</string>
<string name="screen_knock_requests_list_ban_alert_description">"آیا مطمئن هستید که می‌خواهید %1$s را رد و مسدود کنید؟ این کاربر دیگر نمی‌تواند درخواست دسترسی برای پیوستن به این اتاق را بدهد."</string>
<string name="screen_knock_requests_list_ban_alert_title">"رد و تحریم دسترسی"</string>
<string name="screen_knock_requests_list_ban_loading_title">"رد کردن و تحریم دسترسی"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"بله. رد شود"</string>
<string name="screen_knock_requests_list_decline_alert_description">"آیا مطمئن هستید که می‌خواهید درخواست %1$s برای پیوستن به این اتاق را رد کنید؟"</string>
<string name="screen_knock_requests_list_decline_alert_title">"رد کردن دسترسی"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"رد و تحریم"</string>
<string name="screen_knock_requests_list_decline_failed_alert_description">"ما نمی‌توانستیم این درخواست را رد کنیم. آیا مایلید دوباره امتحان کنید؟"</string>
<string name="screen_knock_requests_list_decline_failed_alert_title">"شکتس در رد درخواست"</string>
<string name="screen_knock_requests_list_decline_loading_title">"در کردن درخواست پیوستن"</string>
<string name="screen_knock_requests_list_empty_state_description">"وقتی کسی درخواست عضویت در اتاق را می‌دهد، می‌توانید درخواست او را اینجا ببینید."</string>
<string name="screen_knock_requests_list_empty_state_title">"درخواست عضویت در حال بررسی نیست"</string>
<string name="screen_knock_requests_list_initial_loading_title">"در حال بارگیری درخواست‌های عضویت…"</string>
<string name="screen_knock_requests_list_title">"درخواست‌های پیوستن"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="one">"%1$s +%2$d افراد دیگری هم می‌خواهند به این اتاق بپیوندند"</item>
<item quantity="other">"%1$s +%2$d دیگران می‌خواهند به این اتاق بپیوندند"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"دیدن همه"</string>
<string name="screen_room_single_knock_request_accept_button_title">"پذیرش"</string>
<string name="screen_room_single_knock_request_title">"%1$s می‌خواهد به این اتاق بپیوندد"</string>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"هل أنت متأكد من رغبتك في مغادرة هذه المحادثة؟ هذه المحادثة ليست عامة ولن تتمكن من الانضمام إليها مرة أخرى دون دعوة."</string>
<string name="leave_room_alert_empty_subtitle">"أأنت متأكد من رغبتك بخروج الغرفة؟ أنت الشخص الوحيد هنا. وإذا ما غادرت فلن يعود أحد قادراً على الدخول في المستقبل، بما فيهم أنت."</string>
<string name="leave_room_alert_private_subtitle">"أأنت متأكد من رغبتك بخروج الغرفة؟ هذه الغرفة ليست عامة ولن تعود قادراً على الدخول مجدداً بلا دعوة"</string>
<string name="leave_room_alert_select_new_owner_action">"اختيار المالكين"</string>
<string name="leave_room_alert_select_new_owner_subtitle">"أنت المالك الوحيد لهذه الغرفة. يجب أن تنقل الملكية إلى شخص آخر قبل مغادرة الغرفة."</string>
<string name="leave_room_alert_select_new_owner_title">"نقل الملكية"</string>
<string name="leave_room_alert_subtitle">"أأنت متأكد من رغبتك بخروج الغرفة؟"</string>
</resources>
@@ -3,5 +3,8 @@
<string name="leave_conversation_alert_subtitle">"¿Estás seguro de que quieres salir de esta conversación? Esta conversación no es pública y no podrás volver a unirte sin una invitación."</string>
<string name="leave_room_alert_empty_subtitle">"¿Estás seguro de que quieres salir de esta sala? Eres la única persona aquí. Si te vas, nadie podrá unirse en el futuro, ni siquiera tú."</string>
<string name="leave_room_alert_private_subtitle">"¿Seguro que quieres salir de esta sala? Esta sala no es pública y no podrás volver a entrar sin una invitación."</string>
<string name="leave_room_alert_select_new_owner_action">"Elegir propietarios"</string>
<string name="leave_room_alert_select_new_owner_subtitle">"Eres el único propietario de esta sala. Debes transferir la propiedad a otra persona antes de salir de la sala."</string>
<string name="leave_room_alert_select_new_owner_title">"Transferir la propiedad"</string>
<string name="leave_room_alert_subtitle">"¿Seguro que quieres salir de la sala?"</string>
</resources>
@@ -4,6 +4,7 @@
<string name="leave_room_alert_empty_subtitle">"مطمئنید که می‌خواهید این اتاق را ترک کنید؟ تنها فرد این‌جا هستید. در صورت ترک، هیچ‌کسی از جمله خودتان در آینده نخواهد توانست به آن بپیوندد."</string>
<string name="leave_room_alert_private_subtitle">"مطمئنید که می‌خواهید این اتاق را ترک کنید؟ این اتاق عمومی نبوده قادر نخواهید بود بدون دعوت دوباره بپیوندید."</string>
<string name="leave_room_alert_select_new_owner_action">"گزینش مالکان"</string>
<string name="leave_room_alert_select_new_owner_subtitle">"شما تنها مالک این اتاق هستید. قبل از ترک اتاق، باید مالکیت را به شخص دیگری منتقل کنید."</string>
<string name="leave_room_alert_select_new_owner_title">"انتقال مالکیت"</string>
<string name="leave_room_alert_subtitle">"مطمئنید که می‌خواهید این اتاق را ترک کنید؟"</string>
</resources>
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_link_new_device_desktop_scanning_title">"امسح رمز QR ضوئيًا"</string>
<string name="screen_link_new_device_desktop_step1">"افتح %1$s على كمبيوتر محمول أو مكتبي"</string>
<string name="screen_link_new_device_desktop_step3">"امسح رمز QR ضوئيًا باستخدام هذا الجهاز"</string>
<string name="screen_link_new_device_desktop_submit">"جاهز للمسح الضوئي"</string>
<string name="screen_link_new_device_desktop_title">"افتح %1$s على كمبيوتر مكتبي للحصول على رمز QR"</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"الأرقام غير متطابقة"</string>
<string name="screen_link_new_device_enter_number_notice">"أدخل الرمز المكوّن من رقمين"</string>
<string name="screen_link_new_device_enter_number_subtitle">"سيؤكد هذا أن الاتصال بجهازك الآخر آمن."</string>
<string name="screen_link_new_device_enter_number_title">"أدخل الرقم المعروض على جهازك الآخر"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"لا يدعم موفّر حسابك %1$s."</string>
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s غير مدعوم"</string>
<string name="screen_link_new_device_error_not_supported_subtitle">"لا يدعم موفّر حسابك تسجيل الدخول إلى جهاز جديد باستخدام رمز QR."</string>
<string name="screen_link_new_device_error_not_supported_title">"رمز QR غير مدعوم"</string>
<string name="screen_link_new_device_error_request_cancelled_subtitle">"تم إلغاء تسجيل الدخول على الجهاز الآخر."</string>
<string name="screen_link_new_device_error_request_cancelled_title">"تم إلغاء طلب تسجيل الدخول"</string>
<string name="screen_link_new_device_error_request_timeout_subtitle">"انتهت صلاحية تسجيل الدخول. يرجى المحاولة مرة أخرى."</string>
<string name="screen_link_new_device_error_request_timeout_title">"لم يكتمل تسجيل الدخول في الوقت المحدد"</string>
<string name="screen_link_new_device_mobile_step1">"افتح %1$s على الجهاز الآخر"</string>
<string name="screen_link_new_device_mobile_step2">"اختر %1$s"</string>
<string name="screen_link_new_device_mobile_step2_action">"«تسجيل الدخول باستخدام رمز QR»"</string>
<string name="screen_link_new_device_mobile_step3">"امسح رمز QR المعروض هنا باستخدام الجهاز الآخر"</string>
<string name="screen_link_new_device_mobile_title">"افتح %1$s على الجهاز الآخر"</string>
<string name="screen_link_new_device_root_desktop_computer">"كمبيوتر مكتبي"</string>
<string name="screen_link_new_device_root_loading_qr_code">"جارٍ تحميل رمز QR…"</string>
<string name="screen_link_new_device_root_mobile_device">"جهاز محمول"</string>
<string name="screen_link_new_device_root_title">"ما نوع الجهاز الذي تريد ربطه؟"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"يرجى المحاولة مرة أخرى والتأكد من إدخال الرمز المكوّن من رقمين بشكل صحيح. إذا ظلت الأرقام غير متطابقة، فتواصل مع موفّر حسابك."</string>
<string name="screen_link_new_device_wrong_number_title">"الأرقام غير متطابقة"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"تعذّر إنشاء اتصال آمن بالجهاز الجديد. لا تزال أجهزتك الحالية آمنة ولا داعي للقلق بشأنها."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"ماذا الآن؟"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"حاول تسجيل الدخول مجددًا باستخدام رمز QR إذا كانت هذه مشكلة في الشبكة."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"إذا واجهت المشكلة نفسها، جرّب شبكة Wi-Fi مختلفة أو استخدم بيانات الهاتف المحمول بدلًا من Wi-Fi."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"إذا لم ينجح ذلك، سجّل الدخول يدويًا"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"الاتصال غير آمن"</string>
<string name="screen_qr_code_login_device_code_subtitle">"سيُطلب منك إدخال الرقمين الظاهرين على هذا الجهاز."</string>
<string name="screen_qr_code_login_device_code_title">"أدخل الرقم أدناه على جهازك الآخر"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"تم إلغاء تسجيل الدخول على الجهاز الآخر."</string>
<string name="screen_qr_code_login_error_cancelled_title">"تم إلغاء طلب تسجيل الدخول"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"تم رفض تسجيل الدخول على الجهاز الآخر."</string>
<string name="screen_qr_code_login_error_declined_title">"تم رفض تسجيل الدخول"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"لست بحاجة إلى فعل أي شيء آخر."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"جهازك الآخر مسجّل الدخول بالفعل"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"انتهت صلاحية تسجيل الدخول. يرجى المحاولة مرة أخرى."</string>
<string name="screen_qr_code_login_error_expired_title">"لم يكتمل تسجيل الدخول في الوقت المحدد"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"جهازك الآخر لا يدعم تسجيل الدخول إلى %s باستخدام رمز QR.
جرّب تسجيل الدخول يدويًا، أو امسح رمز QR ضوئيًا باستخدام جهاز آخر."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"رمز QR غير مدعوم"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"لا يدعم موفّر حسابك %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s غير مدعوم"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"استخدم رمز QR المعروض على الجهاز الآخر."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"حاول مرة أخرى"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"رمز QR غير صحيح"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"تحتاج إلى منح %1$s إذنًا لاستخدام كاميرا جهازك للمتابعة."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"اسمح بالوصول إلى الكاميرا لمسح رمز QR ضوئيًا"</string>
<string name="screen_qr_code_login_unknown_error_description">"حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى."</string>
<string name="screen_qr_code_login_verify_code_loading">"في انتظار جهازك الآخر"</string>
</resources>
@@ -1,16 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_link_new_device_desktop_scanning_title">"Escanea el código QR"</string>
<string name="screen_link_new_device_desktop_step1">"Abre %1$s en un ordenador"</string>
<string name="screen_link_new_device_desktop_step3">"Escanea el código QR con este dispositivo"</string>
<string name="screen_link_new_device_desktop_submit">"Listo para escanear"</string>
<string name="screen_link_new_device_desktop_title">"Abre %1$s en un ordenador para obtener el código QR"</string>
<string name="screen_link_new_device_desktop_warning">"Solo escanea un código QR después de haber seguido los pasos anteriores en tu propio dispositivo. Nunca escanees un código que te haya enviado otra persona."</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Los números no coinciden"</string>
<string name="screen_link_new_device_enter_number_notice">"Introduce el código de 2 dígitos"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Esto verificará que la conexión con tu otro dispositivo sea segura."</string>
<string name="screen_link_new_device_enter_number_title">"Introduce el número que aparece en tu otro dispositivo"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Tu proveedor de cuentas no es compatible con %1$s."</string>
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s no admitido"</string>
<string name="screen_link_new_device_error_not_supported_subtitle">"El proveedor de tu cuenta no admite el inicio de sesión en un nuevo dispositivo con un código QR."</string>
<string name="screen_link_new_device_error_not_supported_title">"Código QR no admitido"</string>
<string name="screen_link_new_device_error_request_cancelled_subtitle">"El inicio de sesión se canceló en el otro dispositivo."</string>
<string name="screen_link_new_device_error_request_cancelled_title">"Solicitud de inicio de sesión cancelada"</string>
<string name="screen_link_new_device_error_request_timeout_subtitle">"El inicio de sesión ha caducado. Inténtalo de nuevo."</string>
<string name="screen_link_new_device_error_request_timeout_title">"El inicio de sesión no se completó a tiempo"</string>
<string name="screen_link_new_device_mobile_step1">"Abre %1$s en el otro dispositivo"</string>
<string name="screen_link_new_device_mobile_step2">"Selecciona %1$s"</string>
<string name="screen_link_new_device_mobile_step2_action">"«Iniciar sesión con un código QR»"</string>
<string name="screen_link_new_device_mobile_step3">"Escanea el código QR que se muestra aquí con el otro dispositivo"</string>
<string name="screen_link_new_device_mobile_title">"Abre %1$s en el otro dispositivo"</string>
<string name="screen_link_new_device_root_desktop_computer">"Ordenador"</string>
<string name="screen_link_new_device_root_loading_qr_code">"Cargando código QR…"</string>
<string name="screen_link_new_device_root_mobile_device">"Dispositivo móvil"</string>
<string name="screen_link_new_device_root_title">"¿Qué tipo de dispositivo quieres vincular?"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"Inténtalo de nuevo y asegúrate de haber introducido correctamente el código de 2 dígitos. Si los números siguen sin coincidir, ponte en contacto con el proveedor de tu cuenta."</string>
<string name="screen_link_new_device_wrong_number_title">"Los números no coinciden"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"No se pudo establecer una conexión segura con el nuevo dispositivo. Tus dispositivos actuales siguen siendo seguros y no tienes que preocuparte por ellos."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"¿Y ahora qué?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Intenta iniciar sesión de nuevo con un código QR en caso de que se trate de un problema de red"</string>
@@ -23,6 +41,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"Solicitud de inicio de sesión cancelada"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"El inicio de sesión se rechazó en el otro dispositivo."</string>
<string name="screen_qr_code_login_error_declined_title">"Inicio de sesión rechazado"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"No tienes que hacer nada más."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Tu otro dispositivo ya está conectado"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"El inicio de sesión ha caducado. Inténtalo de nuevo."</string>
<string name="screen_qr_code_login_error_expired_title">"El inicio de sesión no se completó a tiempo"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Tu otro dispositivo no admite el inicio de sesión en %s con un código QR.
@@ -1,16 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_link_new_device_desktop_scanning_title">"پویش کد پاس"</string>
<string name="screen_link_new_device_desktop_step1">"%1$s را روی لپ‌تاپ یا رایانه رومیزی باز کنید."</string>
<string name="screen_link_new_device_desktop_step3">"پویش کد پاس با این افزاره"</string>
<string name="screen_link_new_device_desktop_submit">"آمادهٔ پویش"</string>
<string name="screen_link_new_device_desktop_title">"برای دریافت کد QR، %1$s را روی رایانه رومیزی باز کنید."</string>
<string name="screen_link_new_device_desktop_warning">"فقط پس از انجام مراحل بالا در دستگاه خود، کد QR را اسکن کنید. هرگز کدی را که توسط شخص دیگری ارسال شده است، اسکن نکنید."</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"اعداد مطابقت ندارند"</string>
<string name="screen_link_new_device_enter_number_notice">"کد دو رقمی را وارد کنید"</string>
<string name="screen_link_new_device_enter_number_subtitle">"این کار تأیید می‌کند که اتصال به دستگاه دیگر شما ایمن است."</string>
<string name="screen_link_new_device_enter_number_title">"عدد نمایش داده شده در دستگاه دیگر خود را وارد کنید"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"فراهم کنندهٔ حسابتان از %1$s پشتیبانی نمی‌کند."</string>
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s پشتیبانی نمی‌شود"</string>
<string name="screen_link_new_device_error_not_supported_subtitle">"ارائه دهنده حساب شما از ورود به دستگاه جدید با کد QR پشتیبانی نمی‌کند."</string>
<string name="screen_link_new_device_error_not_supported_title">"کد پاس پشتیبانی نمی‌شود"</string>
<string name="screen_link_new_device_error_request_cancelled_subtitle">"ورود روی افزارهٔ دیگر لغو شد."</string>
<string name="screen_link_new_device_error_request_cancelled_title">"درخواست ورد لغو شد"</string>
<string name="screen_link_new_device_error_request_timeout_subtitle">"ورود منقضی شد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_link_new_device_error_request_timeout_title">"ورود در زمان معیّن کامل نشد"</string>
<string name="screen_link_new_device_mobile_step1">"%1$s را در دستگاه دیگر باز کنید"</string>
<string name="screen_link_new_device_mobile_step2">"گزینش %1$s"</string>
<string name="screen_link_new_device_mobile_step2_action">"«ورود با کد QR»"</string>
<string name="screen_link_new_device_mobile_step3">"کد QR نشان داده شده در اینجا را با دستگاه دیگر اسکن کنید"</string>
<string name="screen_link_new_device_mobile_title">"%1$s را در دستگاه دیگر باز کنید"</string>
<string name="screen_link_new_device_root_desktop_computer">"کامپیوتر رومیزی"</string>
<string name="screen_link_new_device_root_loading_qr_code">"در حال بارگذاری کد QR…"</string>
<string name="screen_link_new_device_root_mobile_device">"دستگاه همراه"</string>
<string name="screen_link_new_device_root_title">"چه نوع دستگاهی را می‌خواهید لینک کنید؟"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"لطفاً دوباره امتحان کنید و مطمئن شوید که کد دو رقمی را به درستی وارد کرده‌اید. اگر اعداد هنوز مطابقت ندارند، با ارائه دهنده حساب خود تماس بگیرید."</string>
<string name="screen_link_new_device_wrong_number_title">"اعداد مطابقت ندارند"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزاره‌های موجودتان هنوز امنند و نیازی نیست نگرانشان باشید."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"اکنون چه؟"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"آزمودن ورود دوباره با کد پاس در صورت مشکل شبکه"</string>
@@ -23,6 +41,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"درخواست ورد لغو شد"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"ورود به دست افزارهٔ دیگر رد شد."</string>
<string name="screen_qr_code_login_error_declined_title">"ورود رد شد"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"لازم نیست کار دیگری انجام دهید."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"دستگاه دیگر شما قبلاً وارد سیستم شده است"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"ورود منقضی شد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_qr_code_login_error_expired_title">"ورود در زمان معیّن کامل نشد"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"افزارهٔ دیگرتان از ورود به %s با کد پاس پشتیبانی نمی‌کند.
@@ -5,6 +5,7 @@
<string name="screen_link_new_device_desktop_step3">"Skannaa QR-koodi tällä laitteella"</string>
<string name="screen_link_new_device_desktop_submit">"Valmis skannaamaan"</string>
<string name="screen_link_new_device_desktop_title">"Avaa %1$s pöytätietokoneella saadaksesi QR-koodin"</string>
<string name="screen_link_new_device_desktop_warning">"Skannaa QR-koodi vasta, kun olet noudattanut yllä olevia ohjeita omalla laitteellasi. Älä koskaan skannaa toisen henkilön lähettämää koodia."</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Numerot eivät täsmää"</string>
<string name="screen_link_new_device_enter_number_notice">"Kirjoita 2-numeroinen koodi"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Tämä varmistaa, että yhteys toiseen laitteeseesi on turvallinen."</string>
@@ -5,6 +5,7 @@
<string name="screen_link_new_device_desktop_step3">"Scanner le code QR avec cet appareil"</string>
<string name="screen_link_new_device_desktop_submit">"Prêt à scanner"</string>
<string name="screen_link_new_device_desktop_title">"Ouvrir %1$s sur un ordinateur pour obtenir le code QR"</string>
<string name="screen_link_new_device_desktop_warning">"Ne scannez un code QR qu’après avoir suivi les étapes ci-dessus sur votre propre appareil. Ne scannez jamais un code envoyé par quelqu’un d’autre."</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Les nombres ne correspondent pas"</string>
<string name="screen_link_new_device_enter_number_notice">"Saisissez le code à 2 chiffres"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Cela permettra de vérifier que la connexion à votre autre appareil est sécurisée."</string>
+1
View File
@@ -71,6 +71,7 @@ dependencies {
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.wellknown.api)
implementation(projects.features.enterprise.api)
implementation(libs.coil.compose)
implementation(libs.datetime)
@@ -8,7 +8,7 @@
package io.element.android.features.location.api
import androidx.compose.runtime.staticCompositionLocalOf
import io.element.android.libraries.wellknown.api.MapTilerConfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
/**
* Provides a [MapTilerConfig] for the MapLibre maps.
@@ -37,6 +37,7 @@ import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.internal.StaticMapPlaceholder
import io.element.android.features.location.api.internal.centerBottomEdge
import io.element.android.features.location.api.internal.rememberStaticMapBuilder
@@ -45,7 +46,6 @@ import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.wellknown.api.MapTilerConfig
/**
* Shows a static map image downloaded via a third party service's static maps API.
@@ -8,8 +8,8 @@
package io.element.android.features.location.api.internal
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.BuildConfig
import io.element.android.libraries.wellknown.api.MapTilerConfig
import kotlin.math.roundToInt
/**
@@ -10,8 +10,8 @@
package io.element.android.features.location.api.internal
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.BuildConfig
import io.element.android.libraries.wellknown.api.MapTilerConfig
internal class MapTilerTileServerStyleUriBuilder(
private val baseUrl: String,
@@ -10,8 +10,7 @@ package io.element.android.features.location.api.internal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.element.android.libraries.wellknown.api.MapTilerConfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
/**
* Builds an URL for a 3rd party service provider static maps API.
*/
@@ -11,7 +11,7 @@ package io.element.android.features.location.api.internal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.wellknown.api.MapTilerConfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
/**
* Builds a style URI for a MapLibre compatible tile server.
@@ -22,7 +22,7 @@ interface TileServerStyleUriBuilder {
/**
* Builds the style URI the map view loads its tiles from.
*
* @param customMapStyleUrl the style advertised by the homeserver, or `null` to use the one built into the app.
* @param customMapTilerConfig the custom MapTiler configuration received from the server, or null if not available.
* @param darkMode whether to request the dark map style.
*/
fun build(
@@ -9,7 +9,7 @@
package io.element.android.features.location.api.internal
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.wellknown.api.MapTilerConfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import org.junit.Test
class MapTilerTileServerStyleUriBuilderTest {
@@ -43,6 +43,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.internal.rememberTileStyleUrl
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.libraries.architecture.AsyncData
@@ -50,7 +51,6 @@ import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.wellknown.api.MapTilerConfig
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.map.MapOptions
@@ -22,7 +22,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
import io.element.android.features.location.impl.common.LocationConstraintsCheck
import io.element.android.features.location.impl.common.MapDefaults
@@ -73,7 +73,7 @@ class ShareLocationPresenter(
private val liveLocationShareManager: ActiveLiveLocationShareManager,
private val liveLocationStore: LiveLocationStore,
private val userLocationStateFactory: UserLocationState.Factory,
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
) : Presenter<ShareLocationState> {
@AssistedFactory
fun interface Factory {
@@ -96,7 +96,7 @@ class ShareLocationPresenter(
val currentUser by client.userProfile.collectAsState()
val customMapConfig by produceState(AsyncData.Loading()) {
// Ignore errors
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(client.sessionId).dataOrNull()?.mapTilerConfig)
value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull())
}
val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms ->
perms.sendLiveLocationPermissions()
@@ -8,12 +8,12 @@
package io.element.android.features.location.impl.share
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.userlocation.UserLocationState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.wellknown.api.MapTilerConfig
import kotlinx.collections.immutable.ImmutableList
data class ShareLocationState(
@@ -9,13 +9,13 @@
package io.element.android.features.location.impl.share
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.userlocation.UserLocationState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.wellknown.api.MapTilerConfig
import kotlinx.collections.immutable.persistentListOf
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
@@ -22,7 +22,7 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
@@ -47,7 +47,6 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
@@ -58,18 +57,18 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@AssistedInject class ShowLocationPresenter(
@AssistedInject
class ShowLocationPresenter(
@Assisted private val mode: ShowLocationMode,
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val locationActions: LocationActions,
private val buildMeta: BuildMeta,
private val dateFormatter: DateFormatter,
private val stringProvider: StringProvider,
private val sessionId: SessionId,
private val joinedRoom: JoinedRoom,
private val liveLocationShareManager: ActiveLiveLocationShareManager,
private val userLocationStateFactory: UserLocationState.Factory,
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
) : Presenter<ShowLocationState> {
@AssistedFactory fun interface Factory {
fun create(mode: ShowLocationMode): ShowLocationPresenter
@@ -89,7 +88,7 @@ import kotlinx.coroutines.launch
val customMapTilerConfig by produceState(AsyncData.Loading()) {
// Ignore errors
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(sessionId).dataOrNull()?.mapTilerConfig)
value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull())
}
fun checkLocationConstraints() {
@@ -8,6 +8,7 @@
package io.element.android.features.location.impl.show
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.ui.LocationMarkerData
@@ -17,7 +18,6 @@ import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.wellknown.api.MapTilerConfig
import kotlinx.collections.immutable.ImmutableList
data class ShowLocationState(
@@ -9,6 +9,7 @@
package io.element.android.features.location.impl.show
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.userlocation.UserLocationState
@@ -18,7 +19,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.wellknown.api.MapTilerConfig
import kotlinx.collections.immutable.toImmutableList
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
@@ -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_share_location_live_location_disclaimer_title">"سيُخزَّن سجل موقعك المباشر في الغرفة وسيكون مرئيًا للأعضاء بعد انتهاء الجلسة."</string>
<string name="screen_share_location_live_location_duration_picker_title">"اختر مدة مشاركة موقعك المباشر."</string>
<string name="screen_share_location_live_location_missing_permissions">"ليست لديك صلاحيات لمشاركة موقعك المباشر في هذه الغرفة"</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_share_location_live_location_disclaimer_title">"Tu historial de ubicación en tiempo real se guardará en la sala y será visible para los miembros después de que termine la sesión."</string>
<string name="screen_share_location_live_location_duration_picker_title">"Elige por cuánto tiempo quieres compartir tu ubicación en tiempo real"</string>
<string name="screen_share_location_live_location_missing_permissions">"No tienes permiso para compartir tu ubicación en tiempo real en esta sala."</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_share_location_live_location_disclaimer_title">"سابقه موقعیت مکانی زنده شما در اتاق ذخیره می‌شود و پس از پایان جلسه برای اعضا قابل مشاهده خواهد بود."</string>
<string name="screen_share_location_live_location_duration_picker_title">"مدت زمان اشتراک‌گذاری موقعیت مکانی زنده خود را انتخاب کنید."</string>
<string name="screen_share_location_live_location_missing_permissions">"شما مجوز اشتراک‌گذاری موقعیت مکانی زنده خود را در این اتاق ندارید"</string>
</resources>
@@ -11,7 +11,6 @@ package io.element.android.features.location.impl.share
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
@@ -59,7 +58,7 @@ class DefaultShareLocationEntryPointTest {
sessionId = room.sessionId,
),
userLocationStateFactory = FakeUserLocationStateFactory(),
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
customMapTilerConfigProvider = { Result.success(null) },
)
},
analyticsService = FakeAnalyticsService(),
@@ -19,8 +19,8 @@ import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.aPermissionsState
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
@@ -32,8 +32,6 @@ import io.element.android.features.location.impl.common.ui.LocationConstraintsDi
import io.element.android.features.location.impl.live.LiveLocationStore
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.features.wellknown.test.aMapTilerConfig
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.dateformatter.test.FakeDurationFormatter
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@@ -55,7 +53,6 @@ import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermiss
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
@@ -92,7 +89,7 @@ class ShareLocationPresenterTest {
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
liveLocationStore: LiveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId),
client: FakeMatrixClient = fakeMatrixClient,
remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
customMapTilerConfigProvider: () -> Result<MapTilerConfig?> = { Result.success(null) },
): ShareLocationPresenter = ShareLocationPresenter(
permissionsPresenterFactory = { fakePermissionsPresenter },
room = joinedRoom,
@@ -106,7 +103,7 @@ class ShareLocationPresenterTest {
liveLocationShareManager = liveLocationShareManager,
liveLocationStore = liveLocationStore,
userLocationStateFactory = FakeUserLocationStateFactory(),
remoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider,
customMapTilerConfigProvider = customMapTilerConfigProvider,
)
@Test
@@ -134,9 +131,7 @@ class ShareLocationPresenterTest {
client = FakeMatrixClient(
sessionId = A_USER_ID,
),
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(
getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = mapTilerConfig)) }
)
customMapTilerConfigProvider = { Result.success(mapTilerConfig) }
)
shareLocationPresenter.test {
val state = awaitItem()
@@ -11,7 +11,6 @@ package io.element.android.features.location.impl.show
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationEntryPoint
import io.element.android.features.location.api.ShowLocationMode
@@ -21,7 +20,6 @@ import io.element.android.features.location.impl.common.permissions.FakePermissi
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.services.analytics.test.FakeAnalyticsService
@@ -51,10 +49,9 @@ class DefaultShowLocationEntryPointTest {
dateFormatter = FakeDateFormatter(),
stringProvider = FakeStringProvider(),
joinedRoom = joinedRoom,
sessionId = A_SESSION_ID,
liveLocationShareManager = FakeActiveLiveLocationShareManager(),
userLocationStateFactory = FakeUserLocationStateFactory(),
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
customMapTilerConfigProvider = { Result.success(null) },
)
},
analyticsService = FakeAnalyticsService(),
@@ -12,7 +12,8 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.aPermissionsState
@@ -23,20 +24,15 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
import io.element.android.features.wellknown.test.aMapTilerConfig
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
@@ -69,9 +65,8 @@ class ShowLocationPresenterTest {
),
locationActions: FakeLocationActions = fakeLocationActions,
joinedRoom: JoinedRoom = FakeJoinedRoom(),
sessionId: SessionId = A_SESSION_ID,
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
remoteEnterpriseConfigProvider: FakeRemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
customMapTilerConfigProvider: CustomMapTilerConfigProvider = { Result.success(null) },
) = ShowLocationPresenter(
mode = mode,
permissionsPresenterFactory = { fakePermissionsPresenter },
@@ -80,21 +75,15 @@ class ShowLocationPresenterTest {
dateFormatter = fakeDateFormatter,
stringProvider = FakeStringProvider(),
joinedRoom = joinedRoom,
sessionId = sessionId,
liveLocationShareManager = liveLocationShareManager,
userLocationStateFactory = FakeUserLocationStateFactory(),
remoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider,
customMapTilerConfigProvider = customMapTilerConfigProvider,
)
@Test
fun `present - non-null customMapStyleUrl`() = runTest {
val mapTilerConfig = aMapTilerConfig(apiKey = "A KEY")
val shareLocationPresenter = createShowLocationPresenter(
sessionId = A_SESSION_ID,
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(
getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = mapTilerConfig)) }
)
)
val shareLocationPresenter = createShowLocationPresenter(customMapTilerConfigProvider = { Result.success(mapTilerConfig) })
shareLocationPresenter.test {
val state = awaitItem()
assertThat(state.customMapTilerConfig.isLoading()).isTrue()
@@ -131,12 +120,7 @@ class ShowLocationPresenterTest {
fun `centers on user location`() = runTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
val presenter = createShowLocationPresenter(
sessionId = A_SESSION_ID,
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(
getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = aMapTilerConfig())) }
)
)
val presenter = createShowLocationPresenter(customMapTilerConfigProvider = { Result.success(aMapTilerConfig()) })
presenter.test {
val initialState = awaitItem()
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"المصادقة بالمقاييس الحيوية"</string>
<string name="screen_app_lock_biometric_unlock">"إلغاء القفل باستخدام المقاييس الحيوية"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"إلغاء القفل باستخدام المقاييس الحيوية"</string>
<string name="screen_app_lock_confirm_biometric_authentication_android">"تأكيد المصادقة الحيوية"</string>
<string name="screen_app_lock_forgot_pin">"هل نسيت رمز PIN؟"</string>
<string name="screen_app_lock_settings_change_pin">"تغيير رمز PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"السماح بإلغاء القفل باستخدام المقاييس الحيوية"</string>
<string name="screen_app_lock_settings_remove_pin">"إزالة رمز PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"هل أنت متأكد من رغبتك في إزالة رمز PIN؟"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"إزالة رمز PIN؟"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"السماح بـ %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"أفضّل استخدام رمز PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"وفّر بعض الوقت واستخدم %1$s لإلغاء قفل التطبيق في كل مرة"</string>
<string name="screen_app_lock_setup_choose_pin">"اختر رمز PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"تأكيد رمز PIN"</string>
<string name="screen_app_lock_setup_pin_context">"اقفل %1$s لإضافة مزيد من الأمان إلى محادثاتك.
اختر شيئًا يسهل تذكره. إذا نسيت رمز PIN هذا، فسيتم تسجيل خروجك من التطبيق."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"لا يمكنك اختيار هذا كرمز PIN لأسباب أمنية"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"اختر رمز PIN مختلفًا"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"يُرجى إدخال رمز PIN نفسه مرتين"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"رمزا PIN غير متطابقين"</string>
<string name="screen_app_lock_signout_alert_message">"ستحتاج إلى تسجيل الدخول مرة أخرى وإنشاء رمز PIN جديد للمتابعة"</string>
<string name="screen_app_lock_signout_alert_title">"تتم إزالة هذا الجهاز"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="zero">"لديك %1$d محاولات لإلغاء القفل"</item>
<item quantity="one">"لديك %1$d محاولة لإلغاء القفل"</item>
<item quantity="two">"لديك %1$d محاولتان لإلغاء القفل"</item>
<item quantity="few">"لديك %1$d محاولات لإلغاء القفل"</item>
<item quantity="many">"لديك %1$d محاولة لإلغاء القفل"</item>
<item quantity="other">"لديك %1$d محاولة لإلغاء القفل"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="zero">"رمز PIN غير صحيح. لديك %1$d فرص أخرى"</item>
<item quantity="one">"رمز PIN غير صحيح. لديك %1$d فرصة أخرى"</item>
<item quantity="two">"رمز PIN غير صحيح. لديك %1$d فرصتان أخريان"</item>
<item quantity="few">"رمز PIN غير صحيح. لديك %1$d فرص أخرى"</item>
<item quantity="many">"رمز PIN غير صحيح. لديك %1$d فرصة أخرى"</item>
<item quantity="other">"رمز PIN غير صحيح. لديك %1$d فرصة أخرى"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"استخدام المقاييس الحيوية"</string>
<string name="screen_app_lock_use_pin_android">"استخدام رمز PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"تجري إزالة الجهاز…"</string>
</resources>
@@ -23,7 +23,7 @@ Elige algo que puedas recordar. Si olvidas este PIN, se cerrará la sesión de l
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Por favor ingresa el mismo PIN dos veces"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Los PINs no coinciden"</string>
<string name="screen_app_lock_signout_alert_message">"Tendrás que volver a iniciar sesión y crear un nuevo PIN para continuar"</string>
<string name="screen_app_lock_signout_alert_title">"Se está cerrando tu sesión"</string>
<string name="screen_app_lock_signout_alert_title">"Se está eliminando este dispositivo"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Tienes %1$d intento de desbloqueo"</item>
<item quantity="other">"Tienes %1$d intentos de desbloqueo"</item>
@@ -34,5 +34,5 @@ Elige algo que puedas recordar. Si olvidas este PIN, se cerrará la sesión de l
</plurals>
<string name="screen_app_lock_use_biometric_android">"Usar desbloqueo biométrico"</string>
<string name="screen_app_lock_use_pin_android">"Usar PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Cerrando sesión…"</string>
<string name="screen_signout_in_progress_dialog_content">"Eliminando dispositivo…"</string>
</resources>
@@ -17,7 +17,7 @@
<string name="screen_app_lock_setup_confirm_pin">"Potwierdź PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Zablokuj %1$s, aby zwiększyć bezpieczeństwo swoich czatów.
Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz ten PIN, zostaniesz wylogowany z aplikacji."</string>
Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz tego PINu, zostaniesz wylogowany z aplikacji."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Nie możesz wybrać tego PIN\'u ze względów bezpieczeństwa"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Wybierz inny kod PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Wprowadź ten sam kod PIN dwa razy"</string>
@@ -11,17 +11,14 @@ package io.element.android.features.login.impl.accesscontrol
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.IsEnterpriseBuild
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
import io.element.android.libraries.wellknown.api.WellknownRetriever
@ContributesBinding(AppScope::class)
class DefaultAccountProviderAccessControl(
private val isEnterpriseBuild: IsEnterpriseBuild,
private val enterpriseService: EnterpriseService,
private val wellknownRetrieverFactory: WellknownRetriever.Factory,
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
) : AccountProviderAccessControl {
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
assertIsAllowedToConnectToAccountProvider(
@@ -38,29 +35,19 @@ class DefaultAccountProviderAccessControl(
title: String,
accountProviderUrl: String,
) {
if (enterpriseService.isEnterpriseBuild.not()) {
if (isEnterpriseBuild().not()) {
// Ensure that Element Pro is not required for this account provider
val temporaryMatrixClient = temporaryMatrixClientFactory.create(accountProviderUrl).getOrThrow()
temporaryMatrixClient.use {
val wellknownRetriever = wellknownRetrieverFactory.create(temporaryMatrixClient)
val wellKnown = wellknownRetriever.getElementWellKnown(
host = accountProviderUrl,
// It's expected this is hardcoded to the well-known endpoint
source = EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT,
).dataOrNull()
if (wellKnown?.enforceElementPro == true) {
throw AccountProviderAccessException.NeedElementProException(
unauthorisedAccountProviderTitle = title,
applicationId = ELEMENT_PRO_APPLICATION_ID,
)
}
if (enterpriseService.isElementProEnforced(accountProviderUrl)) {
throw AccountProviderAccessException.NeedElementProException(
unauthorisedAccountProviderTitle = title,
applicationId = ELEMENT_PRO_APPLICATION_ID,
)
}
}
if (enterpriseService.isAllowedToConnectToHomeserver(accountProviderUrl).not()) {
throw AccountProviderAccessException.UnauthorizedAccountProviderException(
unauthorisedAccountProviderTitle = title,
authorisedAccountProviderTitles = enterpriseService.defaultHomeserverList(),
authorisedAccountProviderTitles = enterpriseService.homeserverAllowList(),
)
}
}
@@ -34,7 +34,7 @@ class AccountProviderDataSource(
// The provider used when the user has not selected one: an enterprise/MDM-configured provider,
// else matrix.org. The most recently used provider (from history) can override it, see init.
private val configuredAccountProvider = createAccountProvider(
url = enterpriseService.defaultHomeserverList()
url = enterpriseService.homeserverAllowList()
.firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
?: AuthenticationConfig.MATRIX_ORG_URL
)
@@ -60,7 +60,7 @@ class ChooseAccountProviderPresenter(
val staticAccountProviderList = remember {
// The list cannot contains ANY_ACCOUNT_PROVIDER ("*") and cannot be empty at this point
enterpriseService.defaultHomeserverList()
enterpriseService.homeserverAllowList()
.map { it.ensureProtocol() }
.map { url ->
AccountProvider(
@@ -60,15 +60,15 @@ class OnBoardingPresenter(
override fun present(): OnBoardingState {
val localCoroutineScope = rememberCoroutineScope()
val forcedAccountProvider = remember {
// If defaultHomeserverList() returns a singleton list, this is the default account provider.
// If homeserverAllowList() returns a singleton list, this is the default account provider.
// In this case, the user can sign in using this homeserver, or use QrCode login
enterpriseService.defaultHomeserverList().singleOrNull()
enterpriseService.homeserverAllowList().singleOrNull()
}
val canConnectToAnyHomeserver = remember {
enterpriseService.canConnectToAnyHomeserver()
}
val mustChooseAccountProvider = remember {
!canConnectToAnyHomeserver && enterpriseService.defaultHomeserverList().size > 1
!canConnectToAnyHomeserver && enterpriseService.homeserverAllowList().size > 1
}
val linkAccountProvider by produceState<String?>(initialValue = null) {
// Account provider from the link, if allowed by the enterprise service
@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"تغيير مزوّد الحسابات"</string>
<string name="screen_account_provider_form_hint">"عنوان السيرفر"</string>
<string name="screen_account_provider_form_notice">"أدخِل عبارة بحث أو عنوان نطاق."</string>
<string name="screen_account_provider_form_subtitle">"ابحث عن شركة، أو مجتمع، أو سيرفر خاص."</string>
<string name="screen_account_provider_form_title">"ابحث عن مزوّد حسابات"</string>
<string name="screen_account_provider_signin_subtitle">"هذا المكان الذي تقطن فيه رسائلك — تماماً كاستخدامك مزوّد بريد إلكتروني للاحتفاظ برسائل البريد خاصّتك فيه."</string>
<string name="screen_account_provider_signin_title">"أنت على وشك تسجيل الدخول إلى %s"</string>
<string name="screen_account_provider_signup_subtitle">"هذا المكان الذي تقطن فيه رسائلك — تماماً كاستخدامك مزوّد بريد إلكتروني للاحتفاظ برسائل البريد خاصّتك فيه."</string>
<string name="screen_account_provider_signup_title">"أنت على وشك إنشاء حساب في %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"إن Matrix.org سيرفر ضخم ومجاني في شبكة Matrix العامّة وُجِد من أجل مراسلات آمنة ولا مركزية، يُدار من قِبل مؤسسة Matrix.org."</string>
<string name="screen_change_account_provider_other">"غير ذلك"</string>
<string name="screen_change_account_provider_subtitle">"استخدم مزوّد حسابات آخر كسيرفرك الخاص أو حساب عمل."</string>
<string name="screen_change_account_provider_title">"تغيير مزوّد الحسابات"</string>
<string name="screen_change_server_error_element_pro_required_action_android">"Google Play"</string>
<string name="screen_change_server_error_element_pro_required_message">"تطبيق Element Pro مطلوب على %1$s. يرجى تنزيله من المتجر."</string>
<string name="screen_change_server_error_element_pro_required_title">"Element Pro مطلوب"</string>
<string name="screen_change_server_error_invalid_homeserver">"لم نستطع الوصول لهذا السيرفر. رجاءً تحقق إذا ما كنت أدخلت عنوان الـURL بشكل صحيح. إذا كان الـURL صحيحاً فراسل إدارة السيرفر خاصتك لمزيد من المساعدة."</string>
<string name="screen_change_server_error_invalid_well_known">"الخادم غير متاح بسبب مشكلة في ملف .well-known:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"لا يدعم موفّر الحساب المحدد ميزة Sliding Sync. يلزم ترقية الخادم لاستخدام %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"غير مسموح لـ %1$s بالاتصال بـ %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"تم إعداد هذا التطبيق للسماح بـ: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"موفّر الحساب %1$s غير مسموح به."</string>
<string name="screen_change_server_form_header">"عنوان الـ URL للسيرفر"</string>
<string name="screen_change_server_form_notice">"أدخل عنوان نطاق."</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>
<string name="screen_login_error_refresh_tokens">"تم إعداد هذا الخادم لاستخدام رموز التحديث. وهي غير مدعومة عند استخدام تسجيل الدخول بكلمة المرور."</string>
<string name="screen_login_error_unsupported_authentication">"لا يتيح المخدِّم المختار تسجيلَ الدخول بكلمة سرّ أو OAuth. راجع المسؤول أو اختر مخدِّماً آخر."</string>
<string name="screen_login_form_header">"أدخِل تفاصيلك"</string>
<string name="screen_login_subtitle">"Matrix عبارة عن شبكة مفتوحة للاتصالات الآمنة واللا مركزية."</string>
<string name="screen_login_title">"أهلاً بعودتك!"</string>
<string name="screen_login_title_with_homeserver">"تسجيل الدخول إلى %1$s"</string>
<string name="screen_missing_key_backup_open_element_classic">"فتح Element Classic"</string>
<string name="screen_missing_key_backup_step_1">"افتح Element Classic على جهازك"</string>
<string name="screen_missing_key_backup_step_2_android">"انتقل إلى الإعدادات &gt; الأمان والخصوصية"</string>
<string name="screen_missing_key_backup_step_3_android">"في إدارة مفاتيح التشفير، اختر استرداد الرسائل المشفّرة"</string>
<string name="screen_missing_key_backup_step_4">"اتبع التعليمات لتفعيل تخزين مفاتيحك"</string>
<string name="screen_missing_key_backup_step_5">"عُد إلى %1$s"</string>
<string name="screen_missing_key_backup_title">"فعّل تخزين مفاتيحك قبل المتابعة إلى %1$s"</string>
<string name="screen_onboarding_app_version">"الإصدار %1$s"</string>
<string name="screen_onboarding_checking_account">"جارٍ التحقق من الحساب"</string>
<string name="screen_onboarding_sign_in_manually">"تسجيل الدخول يدوياً"</string>
<string name="screen_onboarding_sign_in_to">"تسجيل الدخول إلى %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"تسجيل الدخول باستخدام رمز QR"</string>
<string name="screen_onboarding_sign_up">"إنشاء حساب"</string>
<string name="screen_onboarding_welcome_back">"مرحبًا بعودتك"</string>
<string name="screen_onboarding_welcome_message">"مرحباً بك في أسرع %1$s على الإطلاق. مزوَّد من أجل سرعةٍ وبساطة."</string>
<string name="screen_onboarding_welcome_subtitle">"مرحباً بك في %1$s. المعزَّز، من أجل سرعةٍ وبساطة."</string>
<string name="screen_onboarding_welcome_title">"Be in your element"</string>
<string name="screen_qr_code_login_connecting_subtitle">"جارٍ إنشاء اتصال آمن"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"تعذّر إنشاء اتصال آمن بالجهاز الجديد. لا تزال أجهزتك الحالية آمنة ولا داعي للقلق بشأنها."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"ماذا الآن؟"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"حاول تسجيل الدخول مجددًا باستخدام رمز QR إذا كانت هذه مشكلة في الشبكة."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"إذا واجهت المشكلة نفسها، جرّب شبكة Wi-Fi مختلفة أو استخدم بيانات الهاتف المحمول بدلًا من Wi-Fi."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"إذا لم ينجح ذلك، سجّل الدخول يدويًا"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"الاتصال غير آمن"</string>
<string name="screen_qr_code_login_device_code_subtitle">"سيُطلب منك إدخال الرقمين الظاهرين على هذا الجهاز."</string>
<string name="screen_qr_code_login_device_code_title">"أدخل الرقم أدناه على جهازك الآخر"</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"سجّل الدخول إلى جهازك الآخر ثم حاول مرة أخرى، أو استخدم جهازًا آخر تم تسجيل الدخول عليه بالفعل."</string>
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"لم يتم تسجيل الدخول على الجهاز الآخر"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"تم إلغاء تسجيل الدخول على الجهاز الآخر."</string>
<string name="screen_qr_code_login_error_cancelled_title">"تم إلغاء طلب تسجيل الدخول"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"تم رفض تسجيل الدخول على الجهاز الآخر."</string>
<string name="screen_qr_code_login_error_declined_title">"تم رفض تسجيل الدخول"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"لست بحاجة إلى فعل أي شيء آخر."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"جهازك الآخر مسجّل الدخول بالفعل"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"انتهت صلاحية تسجيل الدخول. يرجى المحاولة مرة أخرى."</string>
<string name="screen_qr_code_login_error_expired_title">"لم يكتمل تسجيل الدخول في الوقت المحدد"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"جهازك الآخر لا يدعم تسجيل الدخول إلى %s باستخدام رمز QR.
جرّب تسجيل الدخول يدويًا، أو امسح رمز QR ضوئيًا باستخدام جهاز آخر."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"رمز QR غير مدعوم"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"لا يدعم موفّر حسابك %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s غير مدعوم"</string>
<string name="screen_qr_code_login_initial_state_button_title">"جاهز للمسح الضوئي"</string>
<string name="screen_qr_code_login_initial_state_item_1">"افتح %1$s على جهاز سطح مكتب"</string>
<string name="screen_qr_code_login_initial_state_item_2">"انقر على صورتك الرمزية"</string>
<string name="screen_qr_code_login_initial_state_item_3">"اختر %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“ربط جهاز جديد”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"امسح رمز QR ضوئيًا باستخدام هذا الجهاز"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"متاح فقط إذا كان موفّر حسابك يدعم ذلك."</string>
<string name="screen_qr_code_login_initial_state_title">"افتح %1$s على جهاز آخر للحصول على رمز QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"استخدم رمز QR المعروض على الجهاز الآخر."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"حاول مرة أخرى"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"رمز QR غير صحيح"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"الانتقال إلى إعدادات الكاميرا"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"تحتاج إلى منح %1$s إذنًا لاستخدام كاميرا جهازك للمتابعة."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"اسمح بالوصول إلى الكاميرا لمسح رمز QR ضوئيًا"</string>
<string name="screen_qr_code_login_scanning_state_title">"امسح رمز QR ضوئيًا"</string>
<string name="screen_qr_code_login_start_over_button">"البدء من جديد"</string>
<string name="screen_qr_code_login_unknown_error_description">"حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى."</string>
<string name="screen_qr_code_login_verify_code_loading">"في انتظار جهازك الآخر"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"قد يطلب موفّر حسابك الرمز التالي للتحقق من تسجيل الدخول."</string>
<string name="screen_qr_code_login_verify_code_title">"رمز التحقق الخاص بك"</string>
<string name="screen_server_confirmation_change_server">"تغيير مزوّد الحسابات"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"سيرفر خاص بموظّفي Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix عبارة عن شبكة مفتوحة للاتصالات الآمنة واللا مركزية."</string>
<string name="screen_server_confirmation_message_register">"هذا المكان الذي تقطن فيه رسائلك — تماماً كاستخدامك مزوّد بريد إلكتروني للاحتفاظ برسائل البريد خاصّتك فيه."</string>
<string name="screen_server_confirmation_title_login">"أنت على وشك تسجيل الدخول إلى %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"اختر موفّر الحساب"</string>
<string name="screen_server_confirmation_title_register">"أنت على وشك إنشاء حساب في %1$s"</string>
</resources>
@@ -13,6 +13,9 @@
<string name="screen_change_account_provider_other">"Otro"</string>
<string name="screen_change_account_provider_subtitle">"Usa un proveedor de cuentas diferente, como tu propio servidor privado o una cuenta de trabajo."</string>
<string name="screen_change_account_provider_title">"Cambiar proveedor de cuentas"</string>
<string name="screen_change_server_error_element_pro_required_action_android">"Google Play"</string>
<string name="screen_change_server_error_element_pro_required_message">"%1$s requiere la aplicación Element Pro. Descárgala de la tienda."</string>
<string name="screen_change_server_error_element_pro_required_title">"Se requiere Element Pro"</string>
<string name="screen_change_server_error_invalid_homeserver">"No hemos podido acceder a este servidor base. Comprueba que has introducido correctamente la dirección. Si es correcta, ponte en contacto con el administrador de tu servidor base para obtener más ayuda."</string>
<string name="screen_change_server_error_invalid_well_known">"El servidor no está disponible debido a un problema en el archivo .well-known:
%1$s"</string>
@@ -25,7 +28,7 @@
<string name="screen_change_server_subtitle">"¿Cuál es la dirección de tu servidor?"</string>
<string name="screen_change_server_title">"Selecciona tu servidor"</string>
<string name="screen_create_account_title">"Crear cuenta"</string>
<string name="screen_login_error_deactivated_account">"Esta cuenta ha sido desactivada."</string>
<string name="screen_login_error_deactivated_account">"Esta cuenta ha sido eliminada."</string>
<string name="screen_login_error_invalid_credentials">"Usuario y/o contraseña incorrectos"</string>
<string name="screen_login_error_invalid_user_id">"Este no es un id de usuario válido. Formato esperado: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_refresh_tokens">"Este servidor está configurado para utilizar tokens de actualización. Estos no son compatibles cuando se utiliza el inicio de sesión basado en contraseña."</string>
@@ -34,11 +37,20 @@
<string name="screen_login_subtitle">"Matrix es una red abierta para una comunicación segura y descentralizada."</string>
<string name="screen_login_title">"¡Hola de nuevo!"</string>
<string name="screen_login_title_with_homeserver">"Iniciar sesión en %1$s"</string>
<string name="screen_missing_key_backup_open_element_classic">"Abrir Element Classic"</string>
<string name="screen_missing_key_backup_step_1">"Abre Element Classic en tu dispositivo"</string>
<string name="screen_missing_key_backup_step_2_android">"Ve a Ajustes &gt; Seguridad y privacidad"</string>
<string name="screen_missing_key_backup_step_3_android">"En «Gestión de claves criptográficas», selecciona «Recuperación de mensajes cifrados»"</string>
<string name="screen_missing_key_backup_step_4">"Sigue las instrucciones para activar tu almacén de claves"</string>
<string name="screen_missing_key_backup_step_5">"Vuelve a %1$s"</string>
<string name="screen_missing_key_backup_title">"Activa tu almacén de claves antes de continuar con %1$s"</string>
<string name="screen_onboarding_app_version">"Versión %1$s"</string>
<string name="screen_onboarding_checking_account">"Comprobación de la cuenta"</string>
<string name="screen_onboarding_sign_in_manually">"Iniciar sesión manualmente"</string>
<string name="screen_onboarding_sign_in_to">"Iniciar sesión en %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Iniciar sesión con un código QR"</string>
<string name="screen_onboarding_sign_up">"Crear cuenta"</string>
<string name="screen_onboarding_welcome_back">"¡Hola de nuevo!"</string>
<string name="screen_onboarding_welcome_message">"Bienvenido al %1$s más rápido de todos los tiempos. Diseñado para la velocidad y la simplicidad."</string>
<string name="screen_onboarding_welcome_subtitle">"Bienvenido a %1$s. Vitaminado, para mayor rapidez y sencillez."</string>
<string name="screen_onboarding_welcome_title">"Siéntete en tu Elemento"</string>
@@ -57,6 +69,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"Solicitud de inicio de sesión cancelada"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"El inicio de sesión se rechazó en el otro dispositivo."</string>
<string name="screen_qr_code_login_error_declined_title">"Inicio de sesión rechazado"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"No tienes que hacer nada más."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Tu otro dispositivo ya está conectado"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"El inicio de sesión ha caducado. Inténtalo de nuevo."</string>
<string name="screen_qr_code_login_error_expired_title">"El inicio de sesión no se completó a tiempo"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Tu otro dispositivo no admite el inicio de sesión en %s con un código QR.
@@ -14,8 +14,14 @@
<string name="screen_change_account_provider_subtitle">"استفاده از فراهم کنندهٔ حسابی دیگر چون کارساز خصوصی خوتان یا حسابی کاری."</string>
<string name="screen_change_account_provider_title">"تغییر فراهم کنندهٔ حساب"</string>
<string name="screen_change_server_error_element_pro_required_action_android">"پلی گپگل"</string>
<string name="screen_change_server_error_element_pro_required_message">"برنامه Element Pro برای %1$s مورد نیاز است. لطفاً آن را از فروشگاه دانلود کنید."</string>
<string name="screen_change_server_error_element_pro_required_title">"المنت پرو مورد نیاز است"</string>
<string name="screen_change_server_error_invalid_homeserver">"ما نتوانستیم به این کارساز خانگی برسیم. لطفاً بررسی کنید که URL کارساز اصلی را به درستی وارد کرده اید. اگر URL صحیح است، برای کمک بیشتر با مدیر کارساز خانگی خود تماس بگیرید."</string>
<string name="screen_change_server_error_invalid_well_known">"سرور به دلیل مشکلی در فایل .well-known در دسترس نیست: %1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"ارائه دهنده حساب انتخاب شده از همگام سازی کشویی پشتیبانی نمی کند. برای استفاده از %1$s، ارتقاء سرور لازم است."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s اجازه اتصال به %2$s را ندارد."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"این برنامه طوری پیکربندی شده است که اجازه موارد زیر را بدهد: %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"ارائه دهنده حساب %1$s مجاز نیست."</string>
<string name="screen_change_server_form_header">"نشانی کارساز خانگی"</string>
<string name="screen_change_server_form_notice">"ورود نشانی دامنه."</string>
<string name="screen_change_server_subtitle">"نشانی کارسازتان چیست؟"</string>
@@ -30,11 +36,20 @@
<string name="screen_login_subtitle">"ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است."</string>
<string name="screen_login_title">"خوش برگشتید!"</string>
<string name="screen_login_title_with_homeserver">"ورود به %1$s"</string>
<string name="screen_missing_key_backup_open_element_classic">"باز کردن Element کلاسیک"</string>
<string name="screen_missing_key_backup_step_1">"Element Classic را روی دستگاه خود باز کنید"</string>
<string name="screen_missing_key_backup_step_2_android">"به تنظیمات &gt; امنیت و حریم خصوصی بروید"</string>
<string name="screen_missing_key_backup_step_3_android">"در مدیریت کلیدهای رمزنگاری، بازیابی پیام‌های رمزگذاری‌شده را انتخاب کنید."</string>
<string name="screen_missing_key_backup_step_4">"برای فعال کردن فضای ذخیره‌سازی کلید، دستورالعمل‌ها را دنبال کنید"</string>
<string name="screen_missing_key_backup_step_5">"به %1$s برگردید"</string>
<string name="screen_missing_key_backup_title">"قبل از ادامه به %1$s، فضای ذخیره‌سازی کلید خود را فعال کنید"</string>
<string name="screen_onboarding_app_version">"نگارش %1$s"</string>
<string name="screen_onboarding_checking_account">"درحال بررسی حساب…"</string>
<string name="screen_onboarding_sign_in_manually">"ورود دستی"</string>
<string name="screen_onboarding_sign_in_to">"ورود به %1$s"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"ورود با کد QR"</string>
<string name="screen_onboarding_sign_up">"ایجاد حساب"</string>
<string name="screen_onboarding_welcome_back">"خوش برگشتید"</string>
<string name="screen_onboarding_welcome_message">"به سریع‌ترین %1$s خوش آمدید. بازطرّاحی شده برای سرعت و سادگی."</string>
<string name="screen_onboarding_welcome_subtitle">"به %1$s خوش آمدید. بازطرّاحی شده برای سرعت و سادگی."</string>
<string name="screen_onboarding_welcome_title">"در المنتتان باشید"</string>
@@ -53,6 +68,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"درخواست ورد لغو شد"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"ورود به دست افزارهٔ دیگر رد شد."</string>
<string name="screen_qr_code_login_error_declined_title">"ورود رد شد"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"لازم نیست کار دیگری انجام دهید."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"دستگاه دیگر شما قبلاً وارد سیستم شده است"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"ورود منقضی شد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_qr_code_login_error_expired_title">"ورود در زمان معیّن کامل نشد"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"افزارهٔ دیگرتان از ورود به %s با کد پاس پشتیبانی نمی‌کند.
@@ -67,6 +84,7 @@
<string name="screen_qr_code_login_initial_state_item_3">"گزینش %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"«پیوند افزارهٔ جدید»"</string>
<string name="screen_qr_code_login_initial_state_item_4">"پویش کد پاس با این افزاره"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"فقط در صورتی موجود است که ارائه دهنده حساب شما از آن پشتیبانی کند."</string>
<string name="screen_qr_code_login_initial_state_title">"گشودن %1$s روی افزاره‌ای دیگر برای گرفتن کد پاس"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"استفاده از کد پاس نشان داده روی افزارهٔ دیگر."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"تلاش دوباره"</string>
@@ -85,5 +103,6 @@
<string name="screen_server_confirmation_message_login_matrix_dot_org">"ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است."</string>
<string name="screen_server_confirmation_message_register">"جایی که گفت‌وگوهایتان خواهند زیست — درست مثل استفاده‌تان از فراهم کنندهٔ رایانامه‌ای برای نگه داشتن رایانامه‌هایتان."</string>
<string name="screen_server_confirmation_title_login">"دارید به %1$s وارد می‌شوید"</string>
<string name="screen_server_confirmation_title_picker_mode">"ارائه دهنده حساب را انتخاب کنید"</string>
<string name="screen_server_confirmation_title_register">"دارید روی %1$s حساب می‌سازید"</string>
</resources>
@@ -11,15 +11,9 @@ package io.element.android.features.login.impl.accesscontrol
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
import io.element.android.features.wellknown.test.FakeWellknownRetriever
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Test
@@ -30,9 +24,7 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = false,
isAllowedToConnectToHomeserver = true,
elementWellKnown = anElementWellKnown(
enforceElementPro = true,
),
enterpriseService = FakeEnterpriseService(isElementProEnforcedResult = { true }),
)
accessControl.expectNeedElementProException()
}
@@ -43,9 +35,7 @@ class DefaultAccountProviderAccessControlTest {
isEnterpriseBuild = false,
// false here.
isAllowedToConnectToHomeserver = false,
elementWellKnown = anElementWellKnown(
enforceElementPro = true,
),
enterpriseService = FakeEnterpriseService(isElementProEnforcedResult = { true }),
)
accessControl.expectNeedElementProException()
}
@@ -55,8 +45,9 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = false,
isAllowedToConnectToHomeserver = true,
elementWellKnown = anElementWellKnown(
enforceElementPro = false,
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true },
isElementProEnforcedResult = { false }
),
)
accessControl.expectAllowed()
@@ -67,19 +58,15 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = false,
isAllowedToConnectToHomeserver = true,
elementWellKnown = anElementWellKnown(
enforceElementPro = null,
),
)
accessControl.expectAllowed()
}
@Test
fun `foss build should allow using account provider twith missing wellknown`() = runTest {
fun `foss build should allow using account provider with missing wellknown`() = runTest {
val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = false,
isAllowedToConnectToHomeserver = true,
elementWellKnown = null,
)
accessControl.expectAllowed()
}
@@ -90,8 +77,10 @@ class DefaultAccountProviderAccessControlTest {
isEnterpriseBuild = false,
isAllowedToConnectToHomeserver = false,
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
elementWellKnown = anElementWellKnown(
enforceElementPro = false,
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { false },
isElementProEnforcedResult = { false },
defaultHomeserverListResult = { listOf(AN_ACCOUNT_PROVIDER_2) },
),
)
accessControl.expectUnauthorizedAccountProviderException()
@@ -102,8 +91,9 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = true,
isAllowedToConnectToHomeserver = true,
elementWellKnown = anElementWellKnown(
enforceElementPro = true,
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true },
isElementProEnforcedResult = { true },
),
)
accessControl.expectAllowed()
@@ -114,8 +104,9 @@ class DefaultAccountProviderAccessControlTest {
val accessControl = createDefaultAccountProviderAccessControl(
isEnterpriseBuild = true,
isAllowedToConnectToHomeserver = true,
elementWellKnown = anElementWellKnown(
enforceElementPro = false,
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true },
isElementProEnforcedResult = { false },
),
)
accessControl.expectAllowed()
@@ -127,8 +118,10 @@ class DefaultAccountProviderAccessControlTest {
isEnterpriseBuild = true,
isAllowedToConnectToHomeserver = false,
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
elementWellKnown = anElementWellKnown(
enforceElementPro = true,
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { false },
isElementProEnforcedResult = { true },
defaultHomeserverListResult = { listOf(AN_ACCOUNT_PROVIDER_2) },
),
)
accessControl.expectUnauthorizedAccountProviderException()
@@ -140,8 +133,10 @@ class DefaultAccountProviderAccessControlTest {
isEnterpriseBuild = true,
isAllowedToConnectToHomeserver = false,
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
elementWellKnown = anElementWellKnown(
enforceElementPro = false,
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { false },
isElementProEnforcedResult = { false },
defaultHomeserverListResult = { listOf(AN_ACCOUNT_PROVIDER_2) },
),
)
accessControl.expectUnauthorizedAccountProviderException()
@@ -151,25 +146,14 @@ class DefaultAccountProviderAccessControlTest {
isEnterpriseBuild: Boolean = false,
isAllowedToConnectToHomeserver: Boolean = false,
allowedAccountProviders: List<String> = emptyList(),
elementWellKnown: ElementWellKnown? = null,
) = DefaultAccountProviderAccessControl(
enterpriseService = FakeEnterpriseService(
isEnterpriseBuild = isEnterpriseBuild,
enterpriseService: FakeEnterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { isAllowedToConnectToHomeserver },
defaultHomeserverListResult = { allowedAccountProviders },
),
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(
wellknownRetriever = FakeWellknownRetriever(
getElementWellKnownResult = { _, _ ->
if (elementWellKnown == null) {
WellknownRetrieverResult.NotFound
} else {
WellknownRetrieverResult.Success(elementWellKnown)
}
},
),
),
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
isElementProEnforcedResult = { false },
)
) = DefaultAccountProviderAccessControl(
isEnterpriseBuild = { isEnterpriseBuild },
enterpriseService = enterpriseService,
)
private fun DefaultAccountProviderAccessControl.expectNeedElementProException() {
@@ -17,22 +17,15 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
import io.element.android.features.wellknown.test.FakeWellknownRetriever
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
import io.element.android.libraries.wellknown.api.WellknownRetriever
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@@ -65,6 +58,7 @@ class ChangeServerPresenterTest {
authenticationService = authenticationService,
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true },
isElementProEnforcedResult = { false },
),
).test {
val initialState = awaitItem()
@@ -87,6 +81,7 @@ class ChangeServerPresenterTest {
createPresenter(
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true },
isElementProEnforcedResult = { false },
),
authenticationService = authenticationService,
).test {
@@ -114,6 +109,7 @@ class ChangeServerPresenterTest {
createPresenter(
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true },
isElementProEnforcedResult = { false },
),
authenticationService = authenticationService,
).test {
@@ -137,6 +133,7 @@ class ChangeServerPresenterTest {
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult,
defaultHomeserverListResult = { listOf("element.io") },
isElementProEnforcedResult = { false },
),
).test {
val initialState = awaitItem()
@@ -160,23 +157,19 @@ class ChangeServerPresenterTest {
@Test
fun `present - change server element pro required error`() = runTest {
val getElementWellKnownResult = lambdaRecorder { homeserver: String, _: EnterpriseRemoteConfigSource ->
assertThat(homeserver).isEqualTo(A_HOMESERVER_URL)
WellknownRetrieverResult.Success(
anElementWellKnown(
enforceElementPro = true,
)
)
}
val isEnterpriseBuildLambda = lambdaRecorder<Boolean> { false }
createPresenter(
wellknownRetriever = FakeWellknownRetriever(
getElementWellKnownResult = getElementWellKnownResult,
),
isEnterpriseBuild = isEnterpriseBuildLambda,
enterpriseService = FakeEnterpriseService(
isElementProEnforcedResult = { true },
)
).test {
val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL)
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider))
// Skip loading state
skipItems(1)
val failureState = awaitItem()
assertThat(
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).unauthorisedAccountProviderTitle
@@ -184,7 +177,7 @@ class ChangeServerPresenterTest {
assertThat(
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).applicationId
).isEqualTo("io.element.enterprise")
getElementWellKnownResult.assertions().isCalledOnce()
isEnterpriseBuildLambda.assertions().isCalledOnce()
}
}
@@ -238,7 +231,7 @@ class ChangeServerPresenterTest {
val permissionsPresenter = FakePermissionsPresenter()
createPresenter(
authenticationService = authenticationService,
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }),
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }, isElementProEnforcedResult = { false }),
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
permissionsPresenter = permissionsPresenter,
).test {
@@ -260,16 +253,15 @@ class ChangeServerPresenterTest {
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(),
enterpriseService: EnterpriseService = FakeEnterpriseService(),
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
permissionsPresenter: FakePermissionsPresenter = FakePermissionsPresenter(),
isEnterpriseBuild: () -> Boolean = { false },
) = ChangeServerPresenter(
authenticationService = authenticationService,
accountProviderDataSource = accountProviderDataSource,
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
enterpriseService = enterpriseService,
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(wellknownRetriever = wellknownRetriever),
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
isEnterpriseBuild = isEnterpriseBuild,
),
localNetworkPermissionGate = LocalNetworkPermissionGate(
advisor = localNetworkPermissionAdvisor,
@@ -20,11 +20,9 @@ import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissio
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
import io.element.android.libraries.oauth.api.OAuthAction
@@ -456,9 +454,9 @@ class ConfirmAccountProviderPresenterTest {
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true },
isElementProEnforcedResult = { false },
),
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(),
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
isEnterpriseBuild = { false },
),
localNetworkPermissionGate = LocalNetworkPermissionGate(
advisor = FakeLocalNetworkPermissionAdvisor(),
@@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.appconfig.OnBoardingConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.IsEnterpriseBuild
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
@@ -19,8 +20,6 @@ import io.element.android.features.login.impl.accountprovider.SaveAccountProvide
import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
import io.element.android.features.login.impl.login.LoginModePresenter
import io.element.android.features.wellknown.test.FakeWellknownRetriever
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -31,7 +30,6 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.oauth.api.OAuthActionFlow
@@ -44,7 +42,6 @@ import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.libraries.wellknown.api.WellknownRetriever
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.Flow
@@ -189,6 +186,7 @@ class OnBoardingPresenterTest {
enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
isAllowedToConnectToHomeserverResult = { true },
isElementProEnforcedResult = { false },
),
)
presenter.test {
@@ -262,6 +260,7 @@ class OnBoardingPresenterTest {
),
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true },
isElementProEnforcedResult = { false },
),
loginModePresenter = createLoginModePresenter(
authenticationService = authenticationService,
@@ -298,7 +297,7 @@ private fun createPresenter(
),
buildMeta: BuildMeta = aBuildMeta(),
enterpriseService: EnterpriseService = FakeEnterpriseService(),
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
isEnterpriseBuild: IsEnterpriseBuild = { false },
rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
loginModePresenter: LoginModePresenter = createLoginModePresenter(),
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
@@ -310,8 +309,7 @@ private fun createPresenter(
enterpriseService = enterpriseService,
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
enterpriseService = enterpriseService,
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(wellknownRetriever = wellknownRetriever),
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
isEnterpriseBuild = isEnterpriseBuild,
),
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
loginModePresenter = loginModePresenter,
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"أمتأكد من إزالة هذا الجهاز؟"</string>
<string name="screen_signout_confirmation_dialog_submit">"إزالة هذا الجهاز"</string>
<string name="screen_signout_confirmation_dialog_title">"إزالة هذا الجهاز"</string>
<string name="screen_signout_in_progress_dialog_content">"تجري إزالة الجهاز…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"هذا هو جهازك الوحيد. إذا أزلته، فستحتاج إلى مفتاح استرداد لتأكيد هويتك الرقمية واستعادة محادثاتك المشفّرة في المرة التالية التي تسجّل فيها الدخول."</string>
<string name="screen_signout_key_backup_disabled_title">"أنت على وشك فقدان الوصول إلى محادثاتك المشفّرة"</string>
<string name="screen_signout_key_backup_offline_subtitle">"كان النسخ الاحتياطي لمفاتيحك لا يزال جاريًا عندما أصبحت غير متصل. أعد الاتصال حتى يمكن نسخ مفاتيحك احتياطيًا قبل إزالة هذا الجهاز."</string>
<string name="screen_signout_key_backup_offline_title">"لا يزال النسخ الاحتياطي لمفاتيحك جاريًا"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"يُرجى الانتظار حتى تكتمل هذه العملية قبل إزالة هذا الجهاز."</string>
<string name="screen_signout_key_backup_ongoing_title">"لا يزال النسخ الاحتياطي لمفاتيحك جاريًا"</string>
<string name="screen_signout_preference_item">"إزالة هذا الجهاز"</string>
<string name="screen_signout_recovery_disabled_subtitle">"هذا هو جهازك الوحيد. إذا أزلته، فستحتاج إلى مفتاح استرداد لتأكيد هويتك الرقمية واستعادة محادثاتك المشفّرة في المرة التالية التي تسجّل فيها الدخول."</string>
<string name="screen_signout_recovery_disabled_title">"أنت على وشك فقدان الوصول إلى محادثاتك المشفّرة"</string>
<string name="screen_signout_save_recovery_key_subtitle">"هذا هو جهازك الوحيد. إذا أزلته، فستحتاج إلى مفتاح استرداد لتأكيد هويتك الرقمية واستعادة محادثاتك المشفّرة في المرة التالية التي تسجّل فيها الدخول."</string>
<string name="screen_signout_save_recovery_key_title">"تأكد من أن لديك إمكانية الوصول إلى مفتاح الاسترداد الخاص بك قبل إزالة هذا الجهاز"</string>
</resources>
@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"¿Estás seguro de que quieres cerrar sesión?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Cerrar sesión"</string>
<string name="screen_signout_confirmation_dialog_title">"Cerrar sesión"</string>
<string name="screen_signout_in_progress_dialog_content">"Cerrando sesión…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Estás a punto de cerrar tu última sesión. Si cierras sesión ahora, perderás el acceso a tus mensajes cifrados."</string>
<string name="screen_signout_key_backup_disabled_title">"Has desactivado la copia de seguridad"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Se estaba haciendo una copia de seguridad de tus claves cuando te desconectaste. Vuelve a conectarte para que se haga una copia de seguridad de tus claves antes de desconectarte."</string>
<string name="screen_signout_confirmation_dialog_content">"¿Estás seguro de que quieres eliminar este dispositivo?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Eliminar este dispositivo"</string>
<string name="screen_signout_confirmation_dialog_title">"Eliminar este dispositivo"</string>
<string name="screen_signout_in_progress_dialog_content">"Eliminando dispositivo…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Este es tu único dispositivo. Si lo eliminas, necesitarás una clave de recuperación para poder confirmar tu identidad digital y restaurar tus chats cifrados la próxima vez que inicies sesión."</string>
<string name="screen_signout_key_backup_disabled_title">"Estás a punto de perder acceso a tus chats cifrados"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Se estaba haciendo una copia de seguridad de tus claves cuando te desconectaste. Vuelve a conectarte para que se haga una copia de seguridad de tus claves antes de eliminar este dispositivo."</string>
<string name="screen_signout_key_backup_offline_title">"Se sigue guardando una copia de seguridad de tus claves"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Espera a que se complete antes de cerrar sesión."</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Espera a que se complete antes de eliminar este dispositivo."</string>
<string name="screen_signout_key_backup_ongoing_title">"Se sigue guardando una copia de seguridad de tus claves"</string>
<string name="screen_signout_preference_item">"Cerrar sesión"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Estás a punto de cerrar tu última sesión. Si cierras sesión ahora, perderás el acceso a tus mensajes cifrados."</string>
<string name="screen_signout_recovery_disabled_title">"La recuperación no está configurada"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Estás a punto de cerrar tu última sesión. Si cierras la sesión ahora, podrías perder el acceso a tus mensajes cifrados."</string>
<string name="screen_signout_save_recovery_key_title">"¿Has guardado tu clave de recuperación?"</string>
<string name="screen_signout_preference_item">"Eliminar este dispositivo"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Este es tu único dispositivo. Si lo eliminas, necesitarás una clave de recuperación para poder confirmar tu identidad digital y restaurar tus chats cifrados la próxima vez que inicies sesión."</string>
<string name="screen_signout_recovery_disabled_title">"Estás a punto de perder el acceso a tus chats cifrados."</string>
<string name="screen_signout_save_recovery_key_subtitle">"Este es tu único dispositivo. Si lo eliminas, necesitarás una clave de recuperación para poder confirmar tu identidad digital y restaurar tus chats cifrados la próxima vez que inicies sesión."</string>
<string name="screen_signout_save_recovery_key_title">"Asegúrate de tener acceso a tu clave de recuperación antes de eliminar este dispositivo"</string>
</resources>
@@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
@@ -19,6 +20,7 @@ import io.element.android.features.contentscanner.api.ContentScannerService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.matrix.api.media.isPreviewEnabled
import io.element.android.libraries.matrix.api.room.BaseRoom
@@ -30,8 +32,9 @@ class TimelineProtectionPresenter(
private val mediaPreviewService: MediaPreviewService,
private val room: BaseRoom,
private val contentScannerService: ContentScannerService,
private val timelineProtectionStore: TimelineProtectionStore,
) : Presenter<TimelineProtectionState> {
private val allowedEvents = mutableStateSetOf<EventId>()
@Composable
override fun present(): TimelineProtectionState {
val mediaPreviewValue = remember {
@@ -45,7 +48,7 @@ class TimelineProtectionPresenter(
if (isPreviewEnabled) {
ProtectionState.RenderAll
} else {
ProtectionState.RenderOnly(eventIds = timelineProtectionStore.allowedEventIds.toImmutableSet())
ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet())
}
}
}
@@ -53,7 +56,7 @@ class TimelineProtectionPresenter(
fun handleEvent(event: TimelineProtectionEvent) {
when (event) {
is TimelineProtectionEvent.ShowContent -> {
event.eventId?.let(timelineProtectionStore::allowEvent)
allowedEvents += setOfNotNull(event.eventId)
}
is TimelineProtectionEvent.ValidateContent -> {
contentScannerService.scan(event.mediaSources, event.validationState)
@@ -1,32 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.messages.impl.timeline.protection
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.snapshots.SnapshotStateSet
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.EventId
interface TimelineProtectionStore {
val allowedEventIds: SnapshotStateSet<EventId>
fun allowEvent(eventId: EventId)
}
@ContributesBinding(SessionScope::class)
@SingleIn(SessionScope::class)
class DefaultTimelineProtectionStore : TimelineProtectionStore {
private val allowedEvents = mutableStateSetOf<EventId>()
override val allowedEventIds: SnapshotStateSet<EventId> = allowedEvents
override fun allowEvent(eventId: EventId) {
allowedEvents += eventId
}
}
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_event_authenticity_mismatched_sender">"مرسل الحدث لا يطابق مالك الجهاز الذي أرسله."</string>
<string name="crypto_event_authenticity_not_guaranteed">"لا يمكن ضمان أصالة هذه الرسالة المشفّرة على هذا الجهاز."</string>
<string name="crypto_event_authenticity_previously_verified">"تم التشفير بواسطة مستخدم تم التحقق منه سابقًا."</string>
<string name="crypto_event_authenticity_sent_in_clear">"غير مشفّرة."</string>
<string name="crypto_event_authenticity_unknown_device">"تم التشفير بواسطة جهاز غير معروف أو محذوف."</string>
<string name="crypto_event_authenticity_unsigned_device">"تم التشفير بواسطة جهاز لم يتحقق منه مالكه."</string>
<string name="crypto_event_authenticity_unverified_identity">"تم التشفير بواسطة مستخدم لم يتم التحقق منه."</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"قلب الصورة أفقيًا"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"مقلوبة أفقيًا"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"الأصلية"</string>
<string name="screen_image_edition_a11y_flip_image_vertically">"قلب الصورة رأسيًا"</string>
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"مقلوبة رأسيًا"</string>
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"الأصلية"</string>
<string name="screen_image_edition_a11y_rotate_to_the_left">"تدوير الصورة إلى اليسار"</string>
<plurals name="screen_image_edition_a11y_rotation_state">
<item quantity="zero">"%1$d درجات"</item>
<item quantity="one">"%1$d درجة"</item>
<item quantity="two">"%1$d درجتان"</item>
<item quantity="few">"%1$d درجات"</item>
<item quantity="many">"%1$d درجة"</item>
<item quantity="other">"%1$d درجة"</item>
</plurals>
<string name="screen_image_edition_title">"تعديل الصورة"</string>
<string name="screen_media_upload_preview_caption_warning">"قد لا تكون التعليقات التوضيحية مرئية للأشخاص الذين يستخدمون تطبيقات أقدم."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"اضغط لتغيير جودة رفع الفيديو"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"تعذّر رفع الملف."</string>
<string name="screen_media_upload_preview_error_failed_processing">"تعذّرت معالجة الوسائط من أجل رفعها، يرجى المحاولة مجدداً."</string>
<string name="screen_media_upload_preview_error_failed_sending">"تعذّر رفع الوسائط، يرجى المحاولة مجدداً."</string>
<string name="screen_media_upload_preview_error_too_large_message">"الحد الأقصى المسموح به لحجم الملف هو %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"الملف كبير جدًا ولا يمكن رفعه"</string>
<string name="screen_media_upload_preview_item_count">"العنصر %1$d من %2$d"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"تحسين جودة الصورة"</string>
<string name="screen_media_upload_preview_processing">"جارٍ المعالجة…"</string>
<string name="screen_media_upload_preview_title">"إضافة وسائط"</string>
<string name="screen_report_content_block_user">"حظر المستخدم"</string>
<string name="screen_report_content_block_user_hint">"حدِّد على هذا الخيار إذا كنت تريد إخفاء كل الرسائل الحالية والمستقبلية من هذا المستخدم"</string>
<string name="screen_report_content_explanation">"ستُبلَّغ هذه الرسالة إلى إدارة السيرفر خاصتك. لن يكونوا قادرين على قراءة أي رسالة مشفرة."</string>
<string name="screen_report_content_hint">"سبب إبلاغك عن هذا المحتوى"</string>
<string name="screen_room_attachment_source_camera">"الكاميرا"</string>
<string name="screen_room_attachment_source_camera_photo">"التقاط صورة"</string>
<string name="screen_room_attachment_source_camera_video">"تسجيل مقطع فيديو"</string>
<string name="screen_room_attachment_source_files">"مرفق"</string>
<string name="screen_room_attachment_source_gallery">"معرض الصور والفيديوهات"</string>
<string name="screen_room_attachment_source_location">"مشاركة الموقع الجغرافي"</string>
<string name="screen_room_attachment_source_poll">"استفتاء"</string>
<string name="screen_room_attachment_text_formatting">"تنسيق النص"</string>
<string name="screen_room_encrypted_history_banner">"سجل الرسائل غير متوفر حالياً."</string>
<string name="screen_room_encrypted_history_banner_unverified">"سجل الرسائل غير متاح في هذه الغرفة. تحقّق من هذا الجهاز لرؤية سجل رسائلك."</string>
<string name="screen_room_invite_again_alert_message">"هل تريد دعوته مجدداً؟"</string>
<string name="screen_room_invite_again_alert_title">"أنت لوحدك في هذه المحادثة"</string>
<string name="screen_room_mentions_at_room_subtitle">"إشعار الغرفة بأكملها"</string>
<string name="screen_room_mentions_at_room_title">"الجميع"</string>
<string name="screen_room_retry_send_menu_send_again_action">"أرسل مجدداً"</string>
<string name="screen_room_retry_send_menu_title">"فشل إرسال رسالتك"</string>
<string name="screen_room_timeline_add_reaction">"أضف تفاعل"</string>
<string name="screen_room_timeline_beginning_of_room">"هنا بداية %1$s"</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"هنا بداية المحادثة"</string>
<string name="screen_room_timeline_legacy_call">"مكالمة غير مدعومة. اسأل المتصل عما إذا كان بإمكانه استخدام تطبيق Element X الجديد."</string>
<string name="screen_room_timeline_less_reactions">"أظهر أقل"</string>
<string name="screen_room_timeline_message_copied">"نُسِخت الرسالة"</string>
<string name="screen_room_timeline_no_permission_to_post">"لا تملك الإذن بالنشر في هذه الغرفة"</string>
<plurals name="screen_room_timeline_reaction_a11y">
<item quantity="zero">"تفاعل %1$d أعضاء بـ %2$s"</item>
<item quantity="one">"تفاعل %1$d عضو بـ %2$s"</item>
<item quantity="two">"تفاعل %1$d عضوان بـ %2$s"</item>
<item quantity="few">"تفاعل %1$d أعضاء بـ %2$s"</item>
<item quantity="many">"تفاعل %1$d عضوًا بـ %2$s"</item>
<item quantity="other">"تفاعل %1$d عضو بـ %2$s"</item>
</plurals>
<plurals name="screen_room_timeline_reaction_including_you_a11y">
<item quantity="zero">"تفاعلت أنت و%1$d أعضاء بـ %2$s"</item>
<item quantity="one">"تفاعلت أنت و%1$d عضو بـ %2$s"</item>
<item quantity="two">"تفاعلت أنت و%1$d عضوان بـ %2$s"</item>
<item quantity="few">"تفاعلت أنت و%1$d أعضاء بـ %2$s"</item>
<item quantity="many">"تفاعلت أنت و%1$d عضوًا بـ %2$s"</item>
<item quantity="other">"تفاعلت أنت و%1$d عضو بـ %2$s"</item>
</plurals>
<string name="screen_room_timeline_reaction_you_a11y">"تفاعلت بـ %1$s"</string>
<string name="screen_room_timeline_reactions_show_less">"أظهر أقل"</string>
<string name="screen_room_timeline_reactions_show_more">"أظهر المزيد"</string>
<string name="screen_room_timeline_reactions_show_reactions_summary">"اعرض ملخص التفاعلات"</string>
<string name="screen_room_timeline_read_marker_title">"جديدة"</string>
<plurals name="screen_room_timeline_state_changes">
<item quantity="zero">"%1$d تغيير في الغرفة"</item>
<item quantity="one">"%1$d تغيير في الغرفة"</item>
<item quantity="two">"%1$d تغيير في الغرفة"</item>
<item quantity="few">"%1$d تغييرات في الغرفة"</item>
<item quantity="many">"%1$d تغيير في الغرفة"</item>
<item quantity="other">"%1$d تغيير في الغرفة"</item>
</plurals>
<string name="screen_room_timeline_tombstoned_room_action">"الانتقال إلى الغرفة الجديدة"</string>
<string name="screen_room_timeline_tombstoned_room_message">"استُبدلت هذه الغرفة ولم تعد نشطة"</string>
<string name="screen_room_timeline_upgraded_room_action">"عرض الرسائل القديمة"</string>
<string name="screen_room_timeline_upgraded_room_message">"هذه الغرفة هي استمرار لغرفة أخرى"</string>
<plurals name="screen_room_typing_many_members">
<item quantity="zero">"%1$s و%2$s و%3$d آخرون"</item>
<item quantity="one">"%1$s و%2$s و%3$d آخر"</item>
<item quantity="two">"%1$s و%2$s و%3$d آخران"</item>
<item quantity="few">"%1$s و%2$s و%3$d آخرون"</item>
<item quantity="many">"%1$s و%2$s و%3$d آخر"</item>
<item quantity="other">"%1$s و%2$s و%3$d آخر"</item>
</plurals>
<plurals name="screen_room_typing_notification">
<item quantity="zero">"%1$s يكتبون الآن"</item>
<item quantity="one">"%1$s يكتب الآن"</item>
<item quantity="two">"%1$s يكتبان الآن"</item>
<item quantity="few">"%1$s يكتبون الآن"</item>
<item quantity="many">"%1$s يكتبون الآن"</item>
<item quantity="other">"%1$s يكتبون الآن"</item>
</plurals>
<string name="screen_room_typing_two_members">"%1$s و%2$s"</string>
</resources>
@@ -7,9 +7,29 @@
<string name="crypto_event_authenticity_unknown_device">"Cifrado por un dispositivo desconocido o eliminado."</string>
<string name="crypto_event_authenticity_unsigned_device">"Cifrado por un dispositivo no verificado por su propietario."</string>
<string name="crypto_event_authenticity_unverified_identity">"Cifrado por un usuario no verificado."</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Voltear la imagen horizontalmente"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Volteada horizontalmente"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Original"</string>
<string name="screen_image_edition_a11y_flip_image_vertically">"Voltear la imagen verticalmente"</string>
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Volteada verticalmente"</string>
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Original"</string>
<string name="screen_image_edition_a11y_rotate_to_the_left">"Rotar la imagen hacia la izquierda"</string>
<plurals name="screen_image_edition_a11y_rotation_state">
<item quantity="one">"%1$d grado"</item>
<item quantity="other">"%1$d grados"</item>
</plurals>
<string name="screen_image_edition_title">"Editar foto"</string>
<string name="screen_media_upload_preview_caption_warning">"Es posible que las leyendas no sean visibles para las personas que usan aplicaciones más antiguas."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Toca para cambiar la calidad de subida del vídeo"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"No se pudo subir el archivo."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Error al procesar el contenido multimedia, por favor inténtalo de nuevo."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Error al subir el contenido multimedia, por favor inténtalo de nuevo."</string>
<string name="screen_media_upload_preview_error_too_large_message">"El tamaño máximo permitido de archivo es de %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"El archivo es demasiado grande para subirlo"</string>
<string name="screen_media_upload_preview_item_count">"Elemento %1$d de %2$d"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimizar la calidad de la imagen"</string>
<string name="screen_media_upload_preview_processing">"Procesando…"</string>
<string name="screen_media_upload_preview_title">"Añadir contenido multimedia"</string>
<string name="screen_report_content_block_user">"Bloquear usuario"</string>
<string name="screen_report_content_block_user_hint">"Marca esta casilla si quieres ocultar todos los mensajes actuales y futuros de este usuario"</string>
<string name="screen_report_content_explanation">"Se denunciará este mensaje al administrador de tu servidor base. No será capaz de leer ningún mensaje cifrado."</string>
@@ -19,7 +39,7 @@
<string name="screen_room_attachment_source_camera_video">"Grabar video"</string>
<string name="screen_room_attachment_source_files">"Archivo adjunto"</string>
<string name="screen_room_attachment_source_gallery">"Biblioteca de fotos y vídeos"</string>
<string name="screen_room_attachment_source_location">"Ubicación"</string>
<string name="screen_room_attachment_source_location">"Compartir ubicación"</string>
<string name="screen_room_attachment_source_poll">"Encuesta"</string>
<string name="screen_room_attachment_text_formatting">"Formato de texto"</string>
<string name="screen_room_encrypted_history_banner">"El historial de mensajes no está disponible en este momento."</string>
@@ -37,10 +57,23 @@
<string name="screen_room_timeline_less_reactions">"Mostrar menos"</string>
<string name="screen_room_timeline_message_copied">"Mensaje copiado"</string>
<string name="screen_room_timeline_no_permission_to_post">"No tienes permiso para publicar en esta sala"</string>
<plurals name="screen_room_timeline_reaction_a11y">
<item quantity="one">"%1$d miembro reaccionó con %2$s"</item>
<item quantity="other">"%1$d miembros reaccionaron con %2$s"</item>
</plurals>
<plurals name="screen_room_timeline_reaction_including_you_a11y">
<item quantity="one">"Tú y %1$d miembro reaccionasteis con %2$s"</item>
<item quantity="other">"Tú y %1$d miembros reaccionasteis con %2$s"</item>
</plurals>
<string name="screen_room_timeline_reaction_you_a11y">"Reaccionaste con %1$s"</string>
<string name="screen_room_timeline_reactions_show_less">"Mostrar menos"</string>
<string name="screen_room_timeline_reactions_show_more">"Mostrar más"</string>
<string name="screen_room_timeline_reactions_show_reactions_summary">"Mostrar todas las reacciones"</string>
<string name="screen_room_timeline_read_marker_title">"Nuevos"</string>
<plurals name="screen_room_timeline_redacted_messages">
<item quantity="one">"%1$d mensaje eliminado"</item>
<item quantity="other">"%1$d mensajes eliminados"</item>
</plurals>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d cambio en la sala"</item>
<item quantity="other">"%1$d cambios en la sala"</item>
@@ -1,14 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_event_authenticity_mismatched_sender">"فرستنده رویداد با صاحب دستگاهی که آن را ارسال کرده است، مطابقت ندارد."</string>
<string name="crypto_event_authenticity_not_guaranteed">"اعتبار این پیام رمز شده نمی‌تواند روی این افزاره تأیید شود."</string>
<string name="crypto_event_authenticity_previously_verified">"رمز شده به دست کاربری از پیش تأیید شده."</string>
<string name="crypto_event_authenticity_sent_in_clear">"رمز نشده."</string>
<string name="crypto_event_authenticity_unknown_device">"رمز شده به دست افزاره‌ای ناشناخته یا حذف شده."</string>
<string name="crypto_event_authenticity_unsigned_device">"رمز شده به دست افزاره‌ای که از سوی مالکش تأیید نشده."</string>
<string name="crypto_event_authenticity_unverified_identity">"رمز شده به دست کاربری تأیید نشده."</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"تصویر را به صورت افقی بچرخانید"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"به صورت افقی برگردانده شده"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"اصلی"</string>
<string name="screen_image_edition_a11y_flip_image_vertically">"تصویر را به صورت عمودی بچرخانید"</string>
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"به صورت عمودی برگردانده شده"</string>
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"اصلی"</string>
<string name="screen_image_edition_a11y_rotate_to_the_left">"تصویر را به سمت چپ بچرخانید"</string>
<plurals name="screen_image_edition_a11y_rotation_state">
<item quantity="one">"درجه %1$d"</item>
<item quantity="other">"درجه‌های %1$d"</item>
</plurals>
<string name="screen_image_edition_title">"ویرایش عکس"</string>
<string name="screen_media_upload_preview_caption_warning">"ممکن است زیرنویس‌ها برای افرادی که از برنامه‌های قدیمی‌تر استفاده می‌کنند قابل مشاهده نباشند."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"برای تغییر کیفیت آپلود ویدیو، ضربه بزنید"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"فایل قابل آپلود نبود."</string>
<string name="screen_media_upload_preview_error_failed_processing">"پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_media_upload_preview_error_failed_sending">"بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_media_upload_preview_error_too_large_message">"حداکثر اندازه مجاز فایل %1$s است."</string>
<string name="screen_media_upload_preview_error_too_large_title">"فایل برای آپلود خیلی بزرگ است"</string>
<string name="screen_media_upload_preview_item_count">"مورد %1$d از %2$d"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"بهینه سازی کیفیت تصویر"</string>
<string name="screen_media_upload_preview_processing">"پردازش کردن…"</string>
<string name="screen_media_upload_preview_title">"افزودن رسانه"</string>
<string name="screen_report_content_block_user">"انسداد کاربر"</string>
<string name="screen_report_content_block_user_hint">"اگر می‌خواهید همه پیام‌های فعلی و آینده را از این کاربر را پنهان کنید، علامت بزنید"</string>
<string name="screen_report_content_explanation">"این پیام به مدیر کارساز خانگی شما گزارش خواهد شد. آنها قادر به خواندن پیام های رمزگذاری شده نخواهند بود."</string>
@@ -32,13 +53,27 @@
<string name="screen_room_timeline_add_reaction">"افزودن شکلک"</string>
<string name="screen_room_timeline_beginning_of_room">"آغاز %1$s است."</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"این، آغاز گفت‌وگوست."</string>
<string name="screen_room_timeline_legacy_call">"تماس پشتیبانی نشده. بپرسید که آیا تماس گیرنده می‌تواند از برنامه جدید Element X استفاده کند یا خیر."</string>
<string name="screen_room_timeline_less_reactions">"نمایش کم‌تر"</string>
<string name="screen_room_timeline_message_copied">"پیام رونوشت شد"</string>
<string name="screen_room_timeline_no_permission_to_post">"اجازهٔ فرستادن به این اتاق را ندارید"</string>
<plurals name="screen_room_timeline_reaction_a11y">
<item quantity="one">"عضو %1$d با %2$s واکنش نشان داد"</item>
<item quantity="other">"اعضای %1$d با %2$s واکنش نشان دادند"</item>
</plurals>
<plurals name="screen_room_timeline_reaction_including_you_a11y">
<item quantity="one">"شما و عضو %1$d با %2$s واکنش نشان دادید"</item>
<item quantity="other">"شما و اعضای %1$d با %2$s واکنش نشان دادید"</item>
</plurals>
<string name="screen_room_timeline_reaction_you_a11y">"شما با %1$s واکنش نشان دادید"</string>
<string name="screen_room_timeline_reactions_show_less">"نمایش کم‌تر"</string>
<string name="screen_room_timeline_reactions_show_more">"نمایش بیش‌تر"</string>
<string name="screen_room_timeline_reactions_show_reactions_summary">"نمایش خلاصهٔ واکنش‌ها"</string>
<string name="screen_room_timeline_read_marker_title">"جدید"</string>
<plurals name="screen_room_timeline_redacted_messages">
<item quantity="one">"پیام حذف شده %1$d"</item>
<item quantity="other">"پیام‌های حذف شده %1$d"</item>
</plurals>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$dتغییر اتاق"</item>
<item quantity="other">"%1$dتغییر اتاق"</item>
@@ -70,6 +70,10 @@
<string name="screen_room_timeline_reactions_show_more">"Näytä lisää"</string>
<string name="screen_room_timeline_reactions_show_reactions_summary">"Näytä reaktioiden yhteenveto"</string>
<string name="screen_room_timeline_read_marker_title">"Uusi"</string>
<plurals name="screen_room_timeline_redacted_messages">
<item quantity="one">"%1$d poistettu viesti"</item>
<item quantity="other">"%1$d poistettua viestiä"</item>
</plurals>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d muutos huoneeseen"</item>
<item quantity="other">"%1$d muutosta huoneeseen"</item>
@@ -70,6 +70,10 @@
<string name="screen_room_timeline_reactions_show_more">"Afficher plus"</string>
<string name="screen_room_timeline_reactions_show_reactions_summary">"Afficher le résumé des réactions"</string>
<string name="screen_room_timeline_read_marker_title">"Nouveau"</string>
<plurals name="screen_room_timeline_redacted_messages">
<item quantity="one">"%1$d message supprimé"</item>
<item quantity="other">"%1$d messages supprimés"</item>
</plurals>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d changement dans le salon"</item>
<item quantity="other">"%1$d changements dans le salon"</item>
@@ -95,24 +95,6 @@ class TimelineProtectionPresenterTest {
}
}
@Test
fun `present - shown content is restored when the presenter is recreated`() = runTest {
val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Off, hideInviteAvatar = false)
val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig))
val timelineProtectionStore = DefaultTimelineProtectionStore()
createPresenter(mediaPreviewService = mediaPreviewService, timelineProtectionStore = timelineProtectionStore).test {
val initialState = awaitItem()
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf()))
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID))
val finalState = awaitItem()
assertThat(finalState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)))
}
createPresenter(mediaPreviewService = mediaPreviewService, timelineProtectionStore = timelineProtectionStore).test {
val initialState = awaitItem()
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)))
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - validate media scans the media source`() = runTest {
@@ -152,11 +134,9 @@ class TimelineProtectionPresenterTest {
room: BaseRoom = FakeBaseRoom(),
mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
contentScannerService: ContentScannerService = ContentScannerService { _: List<MediaSource>, _: ContentValidationState -> },
timelineProtectionStore: TimelineProtectionStore = DefaultTimelineProtectionStore(),
) = TimelineProtectionPresenter(
mediaPreviewService = mediaPreviewService,
room = room,
contentScannerService = contentScannerService,
timelineProtectionStore = timelineProtectionStore,
)
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="zero">"%1$d بالمئة من إجمالي الأصوات"</item>
<item quantity="one">"%1$d بالمئة من إجمالي الأصوات"</item>
<item quantity="two">"%1$d بالمئة من إجمالي الأصوات"</item>
<item quantity="few">"%1$d بالمئة من إجمالي الأصوات"</item>
<item quantity="many">"%1$d بالمئة من إجمالي الأصوات"</item>
<item quantity="other">"%1$d بالمئة من إجمالي الأصوات"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"ٍسيتم حذف الإختيار السابق"</string>
<string name="a11y_polls_winning_answer">"هذه هي الإجابة الفائزة"</string>
</resources>
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d por ciento de todos los votos"</item>
<item quantity="other">"%1$d por ciento de todos los votos"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Eliminará la selección anterior"</string>
<string name="a11y_polls_winning_answer">"Esta es la respuesta ganadora"</string>
</resources>

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