Merge branch 'develop' into renovate/io.nlopez.compose.rules-detekt-0.x
This commit is contained in:
commit
683f7d4748
587 changed files with 9299 additions and 2438 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>
|
||||
|
|
@ -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>
|
||||
6
features/call/src/main/res/values-ka/translations.xml
Normal file
6
features/call/src/main/res/values-ka/translations.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="call_foreground_service_channel_title_android">"მიმდინარე ზარი"</string>
|
||||
<string name="call_foreground_service_message_android">"დააწკაპუნეთ ზარში დასაბრუნებლად"</string>
|
||||
<string name="call_foreground_service_title_android">"☎️ ზარი მიმდინარეობს"</string>
|
||||
</resources>
|
||||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
|
@ -68,7 +68,7 @@ class CallScreenPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest {
|
||||
val widgetDriver = FakeWidgetDriver()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val widgetProvider = FakeCallWidgetProvider(widgetDriver)
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
|
|
@ -91,7 +91,7 @@ class CallScreenPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - set message interceptor, send and receive messages`() = runTest {
|
||||
val widgetDriver = FakeWidgetDriver()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
widgetDriver = widgetDriver,
|
||||
|
|
@ -119,7 +119,7 @@ class CallScreenPresenterTest {
|
|||
@Test
|
||||
fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
val widgetDriver = FakeWidgetDriver()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
widgetDriver = widgetDriver,
|
||||
|
|
@ -149,7 +149,7 @@ class CallScreenPresenterTest {
|
|||
@Test
|
||||
fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
val widgetDriver = FakeWidgetDriver()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
widgetDriver = widgetDriver,
|
||||
|
|
@ -178,7 +178,7 @@ class CallScreenPresenterTest {
|
|||
@Test
|
||||
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
val widgetDriver = FakeWidgetDriver()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
|
|
@ -201,7 +201,7 @@ class CallScreenPresenterTest {
|
|||
@Test
|
||||
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
val widgetDriver = FakeWidgetDriver()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
|
||||
|
|
@ -229,7 +229,7 @@ class CallScreenPresenterTest {
|
|||
private fun TestScope.createCallScreenPresenter(
|
||||
callType: CallType,
|
||||
navigator: CallScreenNavigator = FakeCallScreenNavigator(),
|
||||
widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
|
||||
widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(),
|
||||
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
|
||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
|
||||
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -76,7 +76,7 @@ class DefaultCallWidgetProviderTest {
|
|||
fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
||||
givenGetWidgetDriverResult(Result.success(FakeWidgetDriver()))
|
||||
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
||||
}
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
|
@ -89,7 +89,7 @@ class DefaultCallWidgetProviderTest {
|
|||
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
||||
givenGetWidgetDriverResult(Result.success(FakeWidgetDriver()))
|
||||
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
||||
}
|
||||
val client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ package io.element.android.features.call.utils
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
||||
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
|
||||
|
||||
class FakeCallWidgetProvider(
|
||||
private val widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
|
||||
private val widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(),
|
||||
private val url: String = "https://call.element.io",
|
||||
) : CallWidgetProvider {
|
||||
var getWidgetCalled = false
|
||||
|
|
|
|||
|
|
@ -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_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_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_title">"საჯარო ოთახი (ნებისმიერი)"</string>
|
||||
<string name="screen_create_room_room_name_label">"ოთახის სახელი"</string>
|
||||
<string name="screen_create_room_title">"ოთახის შექმნა"</string>
|
||||
<string name="screen_create_room_topic_label">"თემა (სურვილისამებრ)"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"ჩატის დაწყების მცდელობისას შეცდომა მოხდა"</string>
|
||||
</resources>
|
||||
|
|
@ -132,9 +132,8 @@ class FtueFlowNode @AssistedInject constructor(
|
|||
lifecycleScope.launch { moveToNextStep() }
|
||||
}
|
||||
}
|
||||
lockScreenEntryPoint.nodeBuilder(this, buildContext)
|
||||
lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup)
|
||||
.callback(callback)
|
||||
.target(LockScreenEntryPoint.Target.Setup)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,18 @@
|
|||
<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_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>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,18 @@
|
|||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Zadejte níže uvedené číslo na svém dalším zařízení"</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_subtitle">"Přihlášení bylo na druhém zařízení zrušeno."</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_title">"Žádost o přihlášení zrušena"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Požadavek na vašem druhém zařízení nebyl přijat."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Přihlášení odmítnuto"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Platnost přihlášení vypršela. Zkuste to prosím znovu."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Přihlášení nebylo dokončeno včas"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zařízení nepodporuje přihlášení k %su pomocí QR kódu.
|
||||
|
||||
Zkuste se přihlásit ručně nebo naskenujte QR kód pomocí jiného zařízení."</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR kód není podporován"</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Váš poskytovatel účtu nepodporuje %1$s."</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s není podporováno"</string>
|
||||
<string name="screen_qr_code_login_initial_state_button_title">"Připraveno ke skenování"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,22 @@
|
|||
<string name="screen_qr_code_login_connection_note_secure_state_title">"La connexion n’est pas sécurisée"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Il vous sera demandé de saisir les deux chiffres affichés sur cet appareil."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Saisissez le nombre ci-dessous sur votre autre appareil"</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_subtitle">"La connexion a été annulée sur l’autre appareil."</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_title">"Demande de connexion annulée"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"La demande sur l’autre appareil n’a pas été acceptée."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Connexion refusée"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"La connexion a pris trop de temps."</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Votre autre appareil ne supporte pas la connexion à %s avec un code QR. Essayer de vous connecter manuellement, ou scanner le code QR avec un autre appareil."</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_title">"Code QR non supporté"</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Votre fournisseur de compte ne supporte pas %1$s."</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s n’est pas supporté"</string>
|
||||
<string name="screen_qr_code_login_initial_state_button_title">"Prêt à scanner"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Ouvrez %1$s sur un ordinateur"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Suivez les instructions affichées"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Scanner le code QR avec cet appareil"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur l’autre appareil."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>
|
||||
|
|
|
|||
11
features/ftue/impl/src/main/res/values-ka/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-ka/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_notification_optin_subtitle">"თქვენ შეგიძლიათ შეცვალოთ თქვენი პარამეტრები მოგვიანებით."</string>
|
||||
<string name="screen_notification_optin_title">"ყველა შეტყობინებაზე შეტყობინებების მიღება"</string>
|
||||
<string name="screen_welcome_bullet_1">"ზარები, გამოკითხვები, ძიება და სხვა დაემატება ამ წლის ბოლოს."</string>
|
||||
<string name="screen_welcome_bullet_2">"დაშიფრული ოთახებისთვის შეტყობინებების ისტორია ჯერ არ არის ხელმისაწვდომი."</string>
|
||||
<string name="screen_welcome_bullet_3">"ჩვენ სიამოვნებით მოვისმინოთ თქვენგან, შეგვატყობინეთ რას ფიქრობთ პარამეტრების გვერდზე."</string>
|
||||
<string name="screen_welcome_button">"დავიწყოთ!"</string>
|
||||
<string name="screen_welcome_subtitle">"აი, რა უნდა იცოდეთ:"</string>
|
||||
<string name="screen_welcome_title">"კეთილი იყოს თქვენი მობრძანება %1$s-ში!"</string>
|
||||
</resources>
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Ligação insegura"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Ser-te-á pedido que insiras os dois dígitos indicados neste dispositivo."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Insere o número abaixo no teu dispositivo"</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_title">"Pedido de início de sessão cancelado"</string>
|
||||
<string name="screen_qr_code_login_initial_state_button_title">"Pronto para ler"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Abre a %1$s num computador"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Carrega no teu avatar"</string>
|
||||
|
|
@ -11,11 +11,12 @@
|
|||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Conexiunea nu este sigură"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Vi se va cere să introduceți cele două cifre afișate pe acest dispozitiv."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Introduceți numărul de mai jos pe celălalt dispozitiv"</string>
|
||||
<string name="screen_qr_code_login_initial_state_button_title">"Gata de scanare"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Deschideți %1$s pe un dispozitiv desktop"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Faceți clic pe avatarul dumneavoastră"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Selectați %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"„Conectați un dispozitiv nou”"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Urmați instrucțiunile afișate"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Scanați codul QR cu acest dispozitiv"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Deschideți %1$s pe un alt dispozitiv pentru a obține codul QR"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Utilizați codul QR afișat pe celălalt dispozitiv."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Încercați din nou"</string>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,24 @@
|
|||
<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_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">"Соблюдайте показанную инструкцию"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Отсканируйте QR-код с помощью этого устройства"</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>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,18 @@
|
|||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Zadajte nižšie uvedené číslo na vašom druhom zariadení"</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_subtitle">"Prihlásenie bolo zrušené na druhom zariadení."</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_title">"Žiadosť o prihlásenie bola zrušená"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Žiadosť na vašom druhom zariadení nebola prijatá."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Prihlásenie bolo odmietnuté"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Platnosť prihlásenia vypršala. Skúste to prosím znova."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Prihlásenie nebolo včas dokončené"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zariadenie nepodporuje prihlásenie do aplikácie %s pomocou QR kódu.
|
||||
|
||||
Skúste sa prihlásiť manuálne alebo naskenujte QR kód pomocou iného zariadenia."</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR kód nie je podporovaný"</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Poskytovateľ vášho účtu nepodporuje %1$s."</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s nie je podporovaný"</string>
|
||||
<string name="screen_qr_code_login_initial_state_button_title">"Pripravené na skenovanie"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,18 @@
|
|||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"You’ll be asked to enter the two digits shown on this device."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"The request on your other device was not accepted."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Sign in declined"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Sign in expired. Please try again."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"The sign in was not completed in time"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Your other device does not support signing in to %s with a QR code.
|
||||
|
||||
Try signing in manually, or scan the QR code with another device."</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR code not supported"</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Your account provider does not support %1$s."</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s not supported"</string>
|
||||
<string name="screen_qr_code_login_initial_state_button_title">"Ready to scan"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package io.element.android.features.ftue.impl.welcome.state
|
||||
|
||||
class FakeWelcomeState : WelcomeScreenState {
|
||||
class FakeWelcomeScreenState : WelcomeScreenState {
|
||||
private var isWelcomeScreenNeeded = true
|
||||
|
||||
override fun isWelcomeScreenNeeded(): Boolean {
|
||||
|
|
@ -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_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>
|
||||
</resources>
|
||||
|
|
@ -192,9 +192,11 @@ class AcceptDeclineInvitePresenterTest {
|
|||
cancelAndConsumeRemainingEvents()
|
||||
}
|
||||
assert(joinRoomFailure)
|
||||
.isCalledExactly(1)
|
||||
.withSequence(
|
||||
listOf(value(A_ROOM_ID), value(emptyList<String>()), value(JoinedRoom.Trigger.Invite))
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
value(A_ROOM_ID),
|
||||
value(emptyList<String>()),
|
||||
value(JoinedRoom.Trigger.Invite)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -221,9 +223,11 @@ class AcceptDeclineInvitePresenterTest {
|
|||
cancelAndConsumeRemainingEvents()
|
||||
}
|
||||
assert(joinRoomSuccess)
|
||||
.isCalledExactly(1)
|
||||
.withSequence(
|
||||
listOf(value(A_ROOM_ID), value(emptyList<String>()), value(JoinedRoom.Trigger.Invite))
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
value(A_ROOM_ID),
|
||||
value(emptyList<String>()),
|
||||
value(JoinedRoom.Trigger.Invite)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ import io.element.android.libraries.core.meta.BuildMeta
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
|
|
@ -96,7 +95,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
|||
}
|
||||
else -> {
|
||||
value = ContentState.Loading(roomIdOrAlias)
|
||||
val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias())
|
||||
val result = matrixClient.getRoomPreviewFromRoomId(roomId, serverNames)
|
||||
value = result.fold(
|
||||
onSuccess = { roomPreview ->
|
||||
roomPreview.toContentState()
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ class JoinRoomPresenterTest {
|
|||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded`() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
getRoomPreviewResult = {
|
||||
getRoomPreviewFromRoomIdResult = { _, _ ->
|
||||
Result.success(
|
||||
RoomPreview(
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -411,7 +411,7 @@ class JoinRoomPresenterTest {
|
|||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
getRoomPreviewResult = {
|
||||
getRoomPreviewFromRoomIdResult = { _, _ ->
|
||||
Result.failure(AN_EXCEPTION)
|
||||
}
|
||||
)
|
||||
|
|
@ -449,7 +449,7 @@ class JoinRoomPresenterTest {
|
|||
@Test
|
||||
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
|
||||
val client = FakeMatrixClient(
|
||||
getRoomPreviewResult = {
|
||||
getRoomPreviewFromRoomIdResult = { _, _ ->
|
||||
Result.failure(Exception("403"))
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="leave_room_alert_empty_subtitle">"დარწმუნებული ბრძანდებით, რომ ამ ოთახის დატოვება გსურთ? თქვენ აქ მარტო ხართ და ჩატის დატოვებისას აქ თქვენს ჩათვლით ვერავინ ვერ გაწევრიანდება."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"დარწმუნებული ბრძანდებით, რომ ამ ოთახის დატოვება გსურთ? ეს ოთახი არ არის საჯარო და მოწვევის გარეშე ვერ შეძლებთ ხელახლა გაწევრიანებას."</string>
|
||||
<string name="leave_room_alert_subtitle">"დარწმუნებული ბრძანდებით, რომ ოთახის დატოვება გსურთ?"</string>
|
||||
</resources>
|
||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.location.impl.common.permissions
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
class PermissionsPresenterFake : PermissionsPresenter {
|
||||
class FakePermissionsPresenter : PermissionsPresenter {
|
||||
val events = mutableListOf<PermissionsEvents>()
|
||||
|
||||
private fun handleEvent(event: PermissionsEvents) {
|
||||
|
|
@ -24,9 +24,9 @@ import im.vector.app.features.analytics.plan.Composer
|
|||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.aPermissionsState
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
|
|
@ -45,7 +45,7 @@ class SendLocationPresenterTest {
|
|||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private val permissionsPresenterFake = PermissionsPresenterFake()
|
||||
private val fakePermissionsPresenter = FakePermissionsPresenter()
|
||||
private val fakeMatrixRoom = FakeMatrixRoom()
|
||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
||||
|
|
@ -53,7 +53,7 @@ class SendLocationPresenterTest {
|
|||
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
||||
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
|
||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||
override fun create(permissions: List<String>): PermissionsPresenter = permissionsPresenterFake
|
||||
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
||||
},
|
||||
room = fakeMatrixRoom,
|
||||
analyticsService = fakeAnalyticsService,
|
||||
|
|
@ -64,7 +64,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `initial state with permissions granted`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.AllGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
@ -90,7 +90,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `initial state with permissions partially granted`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.SomeGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
@ -116,7 +116,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `initial state with permissions denied`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
@ -142,7 +142,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `initial state with permissions denied once`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
|
|
@ -168,7 +168,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `rationale dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
|
|
@ -199,7 +199,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `rationale dialog continue`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
|
|
@ -221,13 +221,13 @@ class SendLocationPresenterTest {
|
|||
|
||||
// Continue the dialog sends permission request to the permissions presenter
|
||||
myLocationState.eventSink(SendLocationEvents.RequestPermissions)
|
||||
assertThat(permissionsPresenterFake.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
|
||||
assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `permission denied dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
@ -258,7 +258,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `share sender location`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.AllGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
@ -314,7 +314,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `share pin location`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
@ -370,7 +370,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `composer context passes through analytics`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
@ -418,7 +418,7 @@ class SendLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `open settings activity`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.aPermissionsState
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
|
@ -38,13 +38,13 @@ class ShowLocationPresenterTest {
|
|||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private val permissionsPresenterFake = PermissionsPresenterFake()
|
||||
private val fakePermissionsPresenter = FakePermissionsPresenter()
|
||||
private val fakeLocationActions = FakeLocationActions()
|
||||
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
||||
private val location = Location(1.23, 4.56, 7.8f)
|
||||
private val presenter = ShowLocationPresenter(
|
||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||
override fun create(permissions: List<String>): PermissionsPresenter = permissionsPresenterFake
|
||||
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
||||
},
|
||||
fakeLocationActions,
|
||||
fakeBuildMeta,
|
||||
|
|
@ -54,7 +54,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `emits initial state with no location permission`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
@ -74,7 +74,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `emits initial state location permission denied once`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
|
|
@ -94,7 +94,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `emits initial state with location permission`() = runTest {
|
||||
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -109,7 +109,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `emits initial state with partial location permission`() = runTest {
|
||||
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -137,7 +137,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `centers on user location`() = runTest {
|
||||
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -165,7 +165,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `rationale dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
|
|
@ -196,7 +196,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `rationale dialog continue`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
|
|
@ -218,13 +218,13 @@ class ShowLocationPresenterTest {
|
|||
|
||||
// Continue the dialog sends permission request to the permissions presenter
|
||||
trackLocationState.eventSink(ShowLocationEvents.RequestPermissions)
|
||||
assertThat(permissionsPresenterFake.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
|
||||
assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `permission denied dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
@ -255,7 +255,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `open settings activity`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
fakePermissionsPresenter.givenState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
|
|
|
|||
|
|
@ -16,17 +16,19 @@
|
|||
|
||||
package io.element.android.features.lockscreen.api
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
interface LockScreenEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: Target): NodeBuilder
|
||||
fun pinUnlockIntent(context: Context): Intent
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun target(target: Target): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
|
|
@ -37,6 +39,5 @@ interface LockScreenEntryPoint : FeatureEntryPoint {
|
|||
enum class Target {
|
||||
Settings,
|
||||
Setup,
|
||||
Unlock
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
features/lockscreen/impl/src/main/AndroidManifest.xml
Normal file
25
features/lockscreen/impl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!--
|
||||
~ Copyright (c) 2024 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity"
|
||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -16,18 +16,20 @@
|
|||
|
||||
package io.element.android.features.lockscreen.impl
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LockScreenEntryPoint.NodeBuilder {
|
||||
var innerTarget: LockScreenEntryPoint.Target = LockScreenEntryPoint.Target.Unlock
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder {
|
||||
val callbacks = mutableListOf<LockScreenEntryPoint.Callback>()
|
||||
|
||||
return object : LockScreenEntryPoint.NodeBuilder {
|
||||
|
|
@ -36,15 +38,9 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
|
|||
return this
|
||||
}
|
||||
|
||||
override fun target(target: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder {
|
||||
innerTarget = target
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
val inputs = LockScreenFlowNode.Inputs(
|
||||
when (innerTarget) {
|
||||
LockScreenEntryPoint.Target.Unlock -> LockScreenFlowNode.NavTarget.Unlock
|
||||
when (navTarget) {
|
||||
LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup
|
||||
LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings
|
||||
}
|
||||
|
|
@ -54,4 +50,8 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pinUnlockIntent(context: Context): Intent {
|
||||
return PinUnlockActivity.newIntent(context)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import io.element.android.anvilannotations.ContributesNode
|
|||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
|
||||
import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode
|
||||
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
|
|
@ -44,20 +43,17 @@ class LockScreenFlowNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
) : BaseFlowNode<LockScreenFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget,
|
||||
initialElement = plugins.filterIsInstance<Inputs>().first().initialNavTarget,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
data class Inputs(
|
||||
val initialNavTarget: NavTarget = NavTarget.Unlock,
|
||||
val initialNavTarget: NavTarget,
|
||||
) : NodeInputs
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Unlock : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object Setup : NavTarget
|
||||
|
||||
|
|
@ -75,10 +71,6 @@ class LockScreenFlowNode @AssistedInject constructor(
|
|||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Unlock -> {
|
||||
val inputs = PinUnlockNode.Inputs(isInAppUnlock = false)
|
||||
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
|
||||
}
|
||||
NavTarget.Setup -> {
|
||||
val callback = OnSetupDoneCallback(plugins())
|
||||
createNode<LockScreenSetupFlowNode>(buildContext, plugins = listOf(callback))
|
||||
|
|
|
|||
|
|
@ -103,13 +103,12 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
|
|||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Unlock -> {
|
||||
val inputs = PinUnlockNode.Inputs(isInAppUnlock = true)
|
||||
val callback = object : PinUnlockNode.Callback {
|
||||
override fun onUnlock() {
|
||||
backstack.newRoot(NavTarget.Settings)
|
||||
}
|
||||
}
|
||||
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
createNode<PinUnlockNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
NavTarget.SetupPin -> {
|
||||
createNode<SetupPinNode>(buildContext)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ fun LockScreenSettingsView(
|
|||
onBackPressed = onBackPressed,
|
||||
modifier = modifier
|
||||
) {
|
||||
PreferenceCategory(showDivider = false) {
|
||||
PreferenceCategory(showTopDivider = false) {
|
||||
PreferenceText(
|
||||
title = stringResource(id = R.string.screen_app_lock_settings_change_pin),
|
||||
onClick = onChangePinClicked
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -40,12 +38,6 @@ class PinUnlockNode @AssistedInject constructor(
|
|||
fun onUnlock()
|
||||
}
|
||||
|
||||
data class Inputs(
|
||||
val isInAppUnlock: Boolean
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
private fun onUnlock() {
|
||||
plugins<Callback>().forEach {
|
||||
it.onUnlock()
|
||||
|
|
@ -62,7 +54,9 @@ class PinUnlockNode @AssistedInject constructor(
|
|||
}
|
||||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = inputs.isInAppUnlock,
|
||||
// UnlockNode is only used for in-app unlock, so we can safely set isInAppUnlock to true.
|
||||
// It's set to false in PinUnlockActivity.
|
||||
isInAppUnlock = true,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana
|
|||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -41,7 +41,7 @@ import javax.inject.Inject
|
|||
class PinUnlockPresenter @Inject constructor(
|
||||
private val pinCodeManager: PinCodeManager,
|
||||
private val biometricUnlockManager: BiometricUnlockManager,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val signOut: SignOut,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val pinUnlockHelper: PinUnlockHelper,
|
||||
) : Presenter<PinUnlockState> {
|
||||
|
|
@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor(
|
|||
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
|
||||
suspend {
|
||||
matrixClient.logout(ignoreSdkError = true)
|
||||
signOut()
|
||||
}.runCatchingUpdatingState(signOutAction)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.lockscreen.impl.unlock.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter
|
||||
import io.element.android.features.lockscreen.impl.unlock.PinUnlockView
|
||||
import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class PinUnlockActivity : AppCompatActivity() {
|
||||
internal companion object {
|
||||
fun newIntent(context: Context): Intent {
|
||||
return Intent(context, PinUnlockActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Inject lateinit var presenter: PinUnlockPresenter
|
||||
@Inject lateinit var lockScreenService: LockScreenService
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
bindings<PinUnlockBindings>().inject(this)
|
||||
setContent {
|
||||
ElementTheme {
|
||||
val state = presenter.present()
|
||||
PinUnlockView(state = state, isInAppUnlock = false)
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
lockScreenService.lockState.collect { state ->
|
||||
if (state == LockScreenLockState.Unlocked) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.lockscreen.impl.unlock.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface PinUnlockBindings {
|
||||
fun inject(activity: PinUnlockActivity)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.lockscreen.impl.unlock.signout
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultSignOut @Inject constructor(
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
) : SignOut {
|
||||
override suspend fun invoke(): String? {
|
||||
val currentSession = authenticationService.getLatestSessionId()
|
||||
return if (currentSession != null) {
|
||||
matrixClientProvider.getOrRestore(currentSession)
|
||||
.getOrThrow()
|
||||
.logout(ignoreSdkError = true)
|
||||
} else {
|
||||
error("No session to sign out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -14,13 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.mentions
|
||||
package io.element.android.features.lockscreen.impl.unlock.signout
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
@Immutable
|
||||
sealed interface MentionSuggestion {
|
||||
data object Room : MentionSuggestion
|
||||
data class Member(val roomMember: RoomMember) : MentionSuggestion
|
||||
interface SignOut {
|
||||
suspend operator fun invoke(): String?
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<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">"პინ კოდის წაშლა"</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_confirm_pin">"დაადასტურეთ 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="one">"თქვენ გაქვთ %1$d მცდელობა განსაბლოკად"</item>
|
||||
<item quantity="other">"თქვენ გაქვთ %1$d მცდელობა განსაბლოკად"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||
<item quantity="one">"არასწორი PIN. თქვენ %1$d შანსი დაგრჩათ"</item>
|
||||
<item quantity="other">"არასწორი PIN. თქვენ %1$d შანსი დაგრჩათ"</item>
|
||||
</plurals>
|
||||
<string name="screen_signout_in_progress_dialog_content">"გასვლა…"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.lockscreen.impl.unlock
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.lockscreen.impl.unlock.signout.DefaultSignOut
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultSignOutTest {
|
||||
private val matrixClient = FakeMatrixClient()
|
||||
private val authenticationService = FakeMatrixAuthenticationService()
|
||||
private val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) })
|
||||
private val sut = DefaultSignOut(authenticationService, matrixClientProvider)
|
||||
|
||||
@Test
|
||||
fun `when no active session then it throws`() = runTest {
|
||||
authenticationService.getLatestSessionIdLambda = { null }
|
||||
val result = runCatching { sut.invoke() }
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with one active session and successful logout on client`() = runTest {
|
||||
val logoutLambda = lambdaRecorder<Boolean, String?> { _: Boolean -> null }
|
||||
authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId }
|
||||
matrixClient.logoutLambda = logoutLambda
|
||||
val result = runCatching { sut.invoke() }
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
assert(logoutLambda).isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with one active session and and failed logout on client`() = runTest {
|
||||
val logoutLambda = lambdaRecorder<Boolean, String?> { _: Boolean -> error("Failed to logout") }
|
||||
authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId }
|
||||
matrixClient.logoutLambda = logoutLambda
|
||||
val result = runCatching { sut.invoke() }
|
||||
assertThat(result.isFailure).isTrue()
|
||||
assert(logoutLambda).isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.lockscreen.impl.unlock
|
||||
|
||||
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeSignOut(
|
||||
var lambda: () -> String? = { null }
|
||||
) : SignOut {
|
||||
override suspend fun invoke(): String? = simulateLongTask {
|
||||
lambda()
|
||||
}
|
||||
}
|
||||
|
|
@ -28,8 +28,10 @@ import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
|||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.pin.model.assertText
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -104,7 +106,9 @@ class PinUnlockPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - forgot pin flow`() = runTest {
|
||||
val presenter = createPinUnlockPresenter(this)
|
||||
val signOutLambda = lambdaRecorder<String?> { null }
|
||||
val signOut = FakeSignOut(signOutLambda)
|
||||
val presenter = createPinUnlockPresenter(this, signOut = signOut)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -131,6 +135,7 @@ class PinUnlockPresenterTest {
|
|||
awaitItem().also { state ->
|
||||
assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java)
|
||||
}
|
||||
assert(signOutLambda).isCalledOnce().withNoParameter()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +147,7 @@ class PinUnlockPresenterTest {
|
|||
scope: CoroutineScope,
|
||||
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
|
||||
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
||||
signOut: SignOut = FakeSignOut(),
|
||||
): PinUnlockPresenter {
|
||||
val pinCodeManager = aPinCodeManager().apply {
|
||||
addCallback(callback)
|
||||
|
|
@ -150,7 +156,7 @@ class PinUnlockPresenterTest {
|
|||
return PinUnlockPresenter(
|
||||
pinCodeManager = pinCodeManager,
|
||||
biometricUnlockManager = biometricUnlockManager,
|
||||
matrixClient = FakeMatrixClient(),
|
||||
signOut = signOut,
|
||||
coroutineScope = scope,
|
||||
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
|
||||
)
|
||||
|
|
|
|||
42
features/login/impl/src/main/res/values-ka/translations.xml
Normal file
42
features/login/impl/src/main/res/values-ka/translations.xml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?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_invalid_homeserver">"ჩვენ ვერ მივაღწიეთ ამ სახლის სერვერს. გთხოვთ, შეამოწმოთ, რომ სწორად შეიყვანეთ სახლის სერვერის URL. თუ URL სწორია, დაუკავშირდით თქვენი სახლის სერვერის ადმინისტრატორს დამატებითი დახმარებისთვის."</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"ამჟამად ეს სერვერი მხარს არ უჭერს \"sliding sync\"-ს."</string>
|
||||
<string name="screen_change_server_form_header">"სახლის სერვერის URL"</string>
|
||||
<string name="screen_change_server_form_notice">"თქვენ შეგიძლიათ დაუკავშირდეთ მხოლოდ იმ სერვერს, რომელიც მხარს უჭერს \"sliding sync\"-ს. თქვენი სახლის სერვერის ადმინისტრატორს დასჭირდება მისი კონფიგურაცია.%1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"რა არის თქვენი სერვერის მისამართი?"</string>
|
||||
<string name="screen_change_server_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_unsupported_authentication">"მოცემული სახლის სერვერი მხარს არ უჭერს პაროლით ან OIDC-ით შესვლას. გთხოვთ, დაუკავშირდეთ თქვენს ადმინისტრატორს ან აარჩიეთ სხვა სახლის სერვერი."</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_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_register">"თქვენ აპირებთ ანგარიშის შექმნას %1$s-ში"</string>
|
||||
<string name="screen_waitlist_message">"ახლა დიდი მოთხოვნაა %1$s-ზე %2$s-ში. დაბრუნდით რამდენიმე დღეში და სცადეთ ერთხელაც.
|
||||
|
||||
მადლობა მოთმენისათვის!"</string>
|
||||
<string name="screen_waitlist_message_success">"კეთილი იყოს თქვენი მობრძანება %1$s-ში!"</string>
|
||||
<string name="screen_waitlist_title">"თითქმის მზადაა."</string>
|
||||
<string name="screen_waitlist_title_success">"თქვენ შეხვედით."</string>
|
||||
</resources>
|
||||
|
|
@ -14,6 +14,8 @@
|
|||
<string name="screen_change_account_provider_subtitle">"Använd en annan kontoleverantör, till exempel din egen privata server eller ett jobbkonto."</string>
|
||||
<string name="screen_change_account_provider_title">"Byt kontoleverantör"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Vi kunde inte nå den här hemservern. Kontrollera att du har angett hemserverns URL korrekt. Om URL:en är korrekt kontaktar du administratören för hemservern för ytterligare hjälp."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Sliding Sync är inte tillgängligt på grund av ett problem i well-known-filen:
|
||||
%1$s"</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"Den här servern stöder för närvarande inte sliding sync."</string>
|
||||
<string name="screen_change_server_form_header">"Hemserverns URL"</string>
|
||||
<string name="screen_change_server_form_notice">"Du kan bara ansluta till en befintlig server som stöder sliding sync. Din hemserveradministratör måste konfigurera det. %1$s"</string>
|
||||
|
|
@ -22,6 +24,7 @@
|
|||
<string name="screen_login_error_deactivated_account">"Detta konto har avaktiverats."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Felaktigt användarnamn och/eller lösenord"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Detta är inte en giltig användaridentifierare. Förväntat format: \'@användare:hemserver.org\'"</string>
|
||||
<string name="screen_login_error_refresh_tokens">"Den här servern är konfigurerad för att använda uppdateringstokens. Dessa stöds inte när du använder lösenordsbaserad inloggning."</string>
|
||||
<string name="screen_login_error_unsupported_authentication">"Den valda hemservern stöder inte lösenord eller OIDC-inloggning. Kontakta administratören eller välj en annan hemserver."</string>
|
||||
<string name="screen_login_form_header">"Ange dina uppgifter"</string>
|
||||
<string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
|
|||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -38,7 +38,7 @@ class ChangeServerPresenterTest {
|
|||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = ChangeServerPresenter(
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
AccountProviderDataSource()
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
|
@ -51,7 +51,7 @@ class ChangeServerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - change server ok`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val presenter = ChangeServerPresenter(
|
||||
authenticationService,
|
||||
AccountProviderDataSource()
|
||||
|
|
@ -72,7 +72,7 @@ class ChangeServerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - change server error`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val presenter = ChangeServerPresenter(
|
||||
authenticationService,
|
||||
AccountProviderDataSource()
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import io.element.android.features.login.api.oidc.OidcAction
|
|||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -41,7 +41,7 @@ class OidcPresenterTest {
|
|||
fun `present - initial state`() = runTest {
|
||||
val presenter = OidcPresenter(
|
||||
A_OIDC_DATA,
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -56,7 +56,7 @@ class OidcPresenterTest {
|
|||
fun `present - go back`() = runTest {
|
||||
val presenter = OidcPresenter(
|
||||
A_OIDC_DATA,
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -72,7 +72,7 @@ class OidcPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - go back with failure`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val presenter = OidcPresenter(
|
||||
A_OIDC_DATA,
|
||||
authenticationService,
|
||||
|
|
@ -95,7 +95,7 @@ class OidcPresenterTest {
|
|||
fun `present - user cancels from webview`() = runTest {
|
||||
val presenter = OidcPresenter(
|
||||
A_OIDC_DATA,
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -113,7 +113,7 @@ class OidcPresenterTest {
|
|||
fun `present - login success`() = runTest {
|
||||
val presenter = OidcPresenter(
|
||||
A_OIDC_DATA,
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -128,7 +128,7 @@ class OidcPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - login error`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val presenter = OidcPresenter(
|
||||
A_OIDC_DATA,
|
||||
authenticationService,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -36,7 +36,7 @@ class ChangeAccountProviderPresenterTest {
|
|||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val changeServerPresenter = ChangeServerPresenter(
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
AccountProviderDataSource()
|
||||
)
|
||||
val presenter = ChangeAccountProviderPresenter(
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
|||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.waitForPredicate
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -57,7 +57,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - continue password login`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
)
|
||||
|
|
@ -79,7 +79,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - continue oidc`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
)
|
||||
|
|
@ -101,7 +101,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - oidc - cancel with failure`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val defaultOidcActionFlow = DefaultOidcActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
|
|
@ -129,7 +129,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - oidc - cancel with success`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val defaultOidcActionFlow = DefaultOidcActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
|
|
@ -156,7 +156,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - oidc - success with failure`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val defaultOidcActionFlow = DefaultOidcActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
|
|
@ -186,7 +186,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - oidc - success with success`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val defaultOidcActionFlow = DefaultOidcActionFlow()
|
||||
val defaultLoginUserStory = DefaultLoginUserStory().apply {
|
||||
setLoginFlowIsDone(false)
|
||||
|
|
@ -219,7 +219,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - submit fails`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
)
|
||||
|
|
@ -238,7 +238,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - clear error`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
)
|
||||
|
|
@ -267,7 +267,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
private fun createConfirmAccountProviderPresenter(
|
||||
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(),
|
||||
matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService(),
|
||||
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||
defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(),
|
||||
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
|
||||
) = ConfirmAccountProviderPresenter(
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.test.A_PASSWORD
|
|||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -42,7 +42,7 @@ class LoginPasswordPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = LoginPasswordPresenter(
|
||||
|
|
@ -63,7 +63,7 @@ class LoginPasswordPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - enter login and password`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = LoginPasswordPresenter(
|
||||
|
|
@ -89,7 +89,7 @@ class LoginPasswordPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - submit`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
|
||||
val presenter = LoginPasswordPresenter(
|
||||
|
|
@ -118,7 +118,7 @@ class LoginPasswordPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - submit with error`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = LoginPasswordPresenter(
|
||||
|
|
@ -146,7 +146,7 @@ class LoginPasswordPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - clear error`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val accountProviderDataSource = AccountProviderDataSource()
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
val presenter = LoginPasswordPresenter(
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import io.element.android.features.login.impl.resolver.network.WellKnownBaseConf
|
|||
import io.element.android.features.login.impl.resolver.network.WellKnownSlidingSyncConfig
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -44,7 +44,7 @@ class SearchAccountProviderPresenterTest {
|
|||
fun `present - initial state`() = runTest {
|
||||
val fakeWellknownRequest = FakeWellknownRequest()
|
||||
val changeServerPresenter = ChangeServerPresenter(
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
AccountProviderDataSource()
|
||||
)
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
|
|
@ -64,7 +64,7 @@ class SearchAccountProviderPresenterTest {
|
|||
fun `present - enter text no result`() = runTest {
|
||||
val fakeWellknownRequest = FakeWellknownRequest()
|
||||
val changeServerPresenter = ChangeServerPresenter(
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
AccountProviderDataSource()
|
||||
)
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
|
|
@ -88,7 +88,7 @@ class SearchAccountProviderPresenterTest {
|
|||
fun `present - enter valid url no wellknown`() = runTest {
|
||||
val fakeWellknownRequest = FakeWellknownRequest()
|
||||
val changeServerPresenter = ChangeServerPresenter(
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
AccountProviderDataSource()
|
||||
)
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
|
|
@ -123,7 +123,7 @@ class SearchAccountProviderPresenterTest {
|
|||
)
|
||||
)
|
||||
val changeServerPresenter = ChangeServerPresenter(
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
AccountProviderDataSource()
|
||||
)
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
|
|
@ -158,7 +158,7 @@ class SearchAccountProviderPresenterTest {
|
|||
)
|
||||
)
|
||||
val changeServerPresenter = ChangeServerPresenter(
|
||||
FakeAuthenticationService(),
|
||||
FakeMatrixAuthenticationService(),
|
||||
AccountProviderDataSource()
|
||||
)
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER
|
|||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -41,7 +41,7 @@ class WaitListPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService().apply {
|
||||
val authenticationService = FakeMatrixAuthenticationService().apply {
|
||||
givenHomeserver(A_HOMESERVER)
|
||||
}
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
|
|
@ -63,7 +63,7 @@ class WaitListPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - attempt login with error`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService().apply {
|
||||
val authenticationService = FakeMatrixAuthenticationService().apply {
|
||||
givenLoginError(A_THROWABLE)
|
||||
}
|
||||
val loginUserStory = DefaultLoginUserStory()
|
||||
|
|
@ -94,7 +94,7 @@ class WaitListPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - attempt login with success`() = runTest {
|
||||
val authenticationService = FakeAuthenticationService()
|
||||
val authenticationService = FakeMatrixAuthenticationService()
|
||||
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
|
||||
val presenter = WaitListPresenter(
|
||||
LoginFormState.Default,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.junitext)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<?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_preference_item">"გამოსვლა"</string>
|
||||
</resources>
|
||||
|
|
@ -144,7 +144,9 @@ class LogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then cancel`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenLogoutError(A_THROWABLE)
|
||||
logoutLambda = { _ ->
|
||||
throw A_THROWABLE
|
||||
}
|
||||
}
|
||||
val presenter = createLogoutPresenter(
|
||||
matrixClient,
|
||||
|
|
@ -170,7 +172,13 @@ class LogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then force`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenLogoutError(A_THROWABLE)
|
||||
logoutLambda = { ignoreSdkError ->
|
||||
if (!ignoreSdkError) {
|
||||
throw A_THROWABLE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
val presenter = createLogoutPresenter(
|
||||
matrixClient,
|
||||
|
|
|
|||
|
|
@ -125,7 +125,9 @@ class DefaultDirectLogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then cancel`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenLogoutError(A_THROWABLE)
|
||||
logoutLambda = { _ ->
|
||||
throw A_THROWABLE
|
||||
}
|
||||
}
|
||||
val presenter = createDefaultDirectLogoutPresenter(
|
||||
matrixClient,
|
||||
|
|
@ -151,7 +153,13 @@ class DefaultDirectLogoutPresenterTest {
|
|||
@Test
|
||||
fun `present - logout with error then force`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenLogoutError(A_THROWABLE)
|
||||
logoutLambda = { ignoreSdkError ->
|
||||
if (!ignoreSdkError) {
|
||||
throw A_THROWABLE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
val presenter = createDefaultDirectLogoutPresenter(
|
||||
matrixClient,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ dependencies {
|
|||
testImplementation(projects.libraries.mediaviewer.test)
|
||||
testImplementation(projects.libraries.testtags)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.test.junitext)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.features.poll.test)
|
||||
testImplementation(projects.features.poll.impl)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.compose.runtime.setValue
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.appconfig.MessageComposerConfig
|
||||
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||
|
|
@ -66,7 +67,6 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum
|
|||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -113,7 +113,6 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val featureFlagsService: FeatureFlagService,
|
||||
private val htmlConverterProvider: HtmlConverterProvider,
|
||||
@Assisted private val navigator: MessagesNavigator,
|
||||
|
|
@ -171,17 +170,15 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
|
||||
val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
|
||||
var showReinvitePrompt by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow.value) {
|
||||
LaunchedEffect(hasDismissedInviteDialog, composerState.textEditorState.hasFocus(), syncUpdateFlow.value) {
|
||||
withContext(dispatchers.io) {
|
||||
showReinvitePrompt = !hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L
|
||||
showReinvitePrompt = !hasDismissedInviteDialog && composerState.textEditorState.hasFocus() && room.isDirect && room.activeMemberCount == 1L
|
||||
}
|
||||
}
|
||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
||||
|
||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||
|
||||
val enableTextFormatting by appPreferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
|
||||
|
||||
var enableVoiceMessages by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(featureFlagsService) {
|
||||
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
|
||||
|
|
@ -194,7 +191,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
action = event.action,
|
||||
targetEvent = event.event,
|
||||
composerState = composerState,
|
||||
enableTextFormatting = enableTextFormatting,
|
||||
enableTextFormatting = composerState.showTextFormatting,
|
||||
timelineState = timelineState,
|
||||
)
|
||||
}
|
||||
|
|
@ -239,7 +236,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
snackbarMessage = snackbarMessage,
|
||||
showReinvitePrompt = showReinvitePrompt,
|
||||
inviteProgress = inviteProgress.value,
|
||||
enableTextFormatting = enableTextFormatting,
|
||||
enableTextFormatting = MessageComposerConfig.ENABLE_RICH_TEXT_EDITING,
|
||||
enableVoiceMessages = enableVoiceMessages,
|
||||
appName = buildMeta.applicationName,
|
||||
callState = callState,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.textcomposer.aRichTextEditorState
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
|
|
@ -99,9 +100,9 @@ fun aMessagesState(
|
|||
userHasPermissionToRedactOther: Boolean = false,
|
||||
userHasPermissionToSendReaction: Boolean = true,
|
||||
composerState: MessageComposerState = aMessageComposerState(
|
||||
richTextEditorState = aRichTextEditorState(initialText = "Hello", initialFocus = true),
|
||||
isFullScreen = false,
|
||||
mode = MessageComposerMode.Normal,
|
||||
textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)),
|
||||
isFullScreen = false,
|
||||
mode = MessageComposerMode.Normal,
|
||||
),
|
||||
voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(),
|
||||
timelineState: TimelineState = aTimelineState(
|
||||
|
|
|
|||
|
|
@ -362,7 +362,7 @@ private fun MessagesViewContent(
|
|||
// Any state change that should trigger a height size should be added to the list of remembered values here.
|
||||
val sheetResizeContentKey = remember { mutableIntStateOf(0) }
|
||||
LaunchedEffect(
|
||||
state.composerState.richTextEditorState.lineCount,
|
||||
state.composerState.textEditorState.lineCount,
|
||||
state.composerState.showTextFormatting,
|
||||
) {
|
||||
sheetResizeContentKey.intValue = Random.nextInt()
|
||||
|
|
@ -439,7 +439,6 @@ private fun MessagesViewComposerBottomSheetContents(
|
|||
state = state.composerState,
|
||||
voiceMessageState = state.voiceMessageComposerState,
|
||||
subcomposing = subcomposing,
|
||||
enableTextFormatting = state.enableTextFormatting,
|
||||
enableVoiceMessages = state.enableVoiceMessages,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
@ -51,8 +52,8 @@ fun MentionSuggestionsPickerView(
|
|||
roomId: RoomId,
|
||||
roomName: String?,
|
||||
roomAvatarData: AvatarData?,
|
||||
memberSuggestions: ImmutableList<MentionSuggestion>,
|
||||
onSuggestionSelected: (MentionSuggestion) -> Unit,
|
||||
memberSuggestions: ImmutableList<ResolvedMentionSuggestion>,
|
||||
onSuggestionSelected: (ResolvedMentionSuggestion) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
|
|
@ -62,8 +63,8 @@ fun MentionSuggestionsPickerView(
|
|||
memberSuggestions,
|
||||
key = { suggestion ->
|
||||
when (suggestion) {
|
||||
is MentionSuggestion.Room -> "@room"
|
||||
is MentionSuggestion.Member -> suggestion.roomMember.userId.value
|
||||
is ResolvedMentionSuggestion.AtRoom -> "@room"
|
||||
is ResolvedMentionSuggestion.Member -> suggestion.roomMember.userId.value
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
|
@ -84,18 +85,18 @@ fun MentionSuggestionsPickerView(
|
|||
|
||||
@Composable
|
||||
private fun RoomMemberSuggestionItemView(
|
||||
memberSuggestion: MentionSuggestion,
|
||||
memberSuggestion: ResolvedMentionSuggestion,
|
||||
roomId: String,
|
||||
roomName: String?,
|
||||
roomAvatar: AvatarData?,
|
||||
onSuggestionSelected: (MentionSuggestion) -> Unit,
|
||||
onSuggestionSelected: (ResolvedMentionSuggestion) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(modifier = modifier.clickable { onSuggestionSelected(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
val avatarSize = AvatarSize.TimelineRoom
|
||||
val avatarData = when (memberSuggestion) {
|
||||
is MentionSuggestion.Room -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
|
||||
is MentionSuggestion.Member -> AvatarData(
|
||||
is ResolvedMentionSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
|
||||
is ResolvedMentionSuggestion.Member -> AvatarData(
|
||||
memberSuggestion.roomMember.userId.value,
|
||||
memberSuggestion.roomMember.displayName,
|
||||
memberSuggestion.roomMember.avatarUrl,
|
||||
|
|
@ -103,13 +104,13 @@ private fun RoomMemberSuggestionItemView(
|
|||
)
|
||||
}
|
||||
val title = when (memberSuggestion) {
|
||||
is MentionSuggestion.Room -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||
is MentionSuggestion.Member -> memberSuggestion.roomMember.displayName
|
||||
is ResolvedMentionSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||
is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.displayName
|
||||
}
|
||||
|
||||
val subtitle = when (memberSuggestion) {
|
||||
is MentionSuggestion.Room -> "@room"
|
||||
is MentionSuggestion.Member -> memberSuggestion.roomMember.userId.value
|
||||
is ResolvedMentionSuggestion.AtRoom -> "@room"
|
||||
is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.userId.value
|
||||
}
|
||||
|
||||
Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp))
|
||||
|
|
@ -159,9 +160,9 @@ internal fun MentionSuggestionsPickerViewPreview() {
|
|||
roomName = "Room",
|
||||
roomAvatarData = null,
|
||||
memberSuggestions = persistentListOf(
|
||||
MentionSuggestion.Room,
|
||||
MentionSuggestion.Member(roomMember),
|
||||
MentionSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
|
||||
ResolvedMentionSuggestion.AtRoom,
|
||||
ResolvedMentionSuggestion.Member(roomMember),
|
||||
ResolvedMentionSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
|
||||
),
|
||||
onSuggestionSelected = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
|||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||
import io.element.android.libraries.textcomposer.model.SuggestionType
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ object MentionSuggestionsProcessor {
|
|||
roomMembersState: MatrixRoomMembersState,
|
||||
currentUserId: UserId,
|
||||
canSendRoomMention: suspend () -> Boolean,
|
||||
): List<MentionSuggestion> {
|
||||
): List<ResolvedMentionSuggestion> {
|
||||
val members = roomMembersState.roomMembers()
|
||||
return when {
|
||||
members.isNullOrEmpty() || suggestion == null -> {
|
||||
|
|
@ -78,7 +79,7 @@ object MentionSuggestionsProcessor {
|
|||
roomMembers: List<RoomMember>?,
|
||||
currentUserId: UserId,
|
||||
canSendRoomMention: Boolean,
|
||||
): List<MentionSuggestion> {
|
||||
): List<ResolvedMentionSuggestion> {
|
||||
return if (roomMembers.isNullOrEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
|
|
@ -96,10 +97,10 @@ object MentionSuggestionsProcessor {
|
|||
.filterUpTo(MAX_BATCH_ITEMS) { member ->
|
||||
isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query)
|
||||
}
|
||||
.map(MentionSuggestion::Member)
|
||||
.map(ResolvedMentionSuggestion::Member)
|
||||
|
||||
if ("room".contains(query) && canSendRoomMention) {
|
||||
listOf(MentionSuggestion.Room) + matchingMembers
|
||||
listOf(ResolvedMentionSuggestion.AtRoom) + matchingMembers
|
||||
} else {
|
||||
matchingMembers
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,15 +18,14 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.libraries.textcomposer.model.Message
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||
|
||||
@Immutable
|
||||
sealed interface MessageComposerEvents {
|
||||
data object ToggleFullScreenState : MessageComposerEvents
|
||||
data class SendMessage(val message: Message) : MessageComposerEvents
|
||||
data object SendMessage : MessageComposerEvents
|
||||
data class SendUri(val uri: Uri) : MessageComposerEvents
|
||||
data object CloseSpecialMode : MessageComposerEvents
|
||||
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
|
||||
|
|
@ -45,5 +44,5 @@ sealed interface MessageComposerEvents {
|
|||
data class Error(val error: Throwable) : MessageComposerEvents
|
||||
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
|
||||
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
|
||||
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
|
||||
data class InsertMention(val mention: ResolvedMentionSuggestion) : MessageComposerEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -29,6 +30,7 @@ import androidx.compose.runtime.mutableStateListOf
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.media3.common.MimeTypes
|
||||
|
|
@ -36,7 +38,6 @@ import androidx.media3.common.util.UnstableApi
|
|||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor
|
||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
|
|
@ -59,17 +60,21 @@ import io.element.android.libraries.mediaupload.api.MediaSender
|
|||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||
import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider
|
||||
import io.element.android.libraries.textcomposer.model.Message
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
|
@ -108,12 +113,27 @@ class MessageComposerPresenter @Inject constructor(
|
|||
|
||||
private val suggestionSearchTrigger = MutableStateFlow<Suggestion?>(null)
|
||||
|
||||
// Used to disable some UI related elements in tests
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal var isTesting: Boolean = false
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal var showTextFormatting: Boolean by mutableStateOf(false)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@Composable
|
||||
override fun present(): MessageComposerState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
// Initially disabled so we don't set focus and text twice
|
||||
var applyFormattingModeChanges by remember { mutableStateOf(false) }
|
||||
val richTextEditorState = richTextEditorStateFactory.remember()
|
||||
if (isTesting) {
|
||||
richTextEditorState.isReadyToProcessActions = true
|
||||
}
|
||||
val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false)
|
||||
|
||||
var isMentionsEnabled by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
isMentionsEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.Mentions)
|
||||
|
|
@ -149,18 +169,20 @@ class MessageComposerPresenter @Inject constructor(
|
|||
val isFullScreen = rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val richTextEditorState = richTextEditorStateFactory.create()
|
||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
|
||||
var showTextFormatting: Boolean by remember { mutableStateOf(false) }
|
||||
|
||||
val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true)
|
||||
|
||||
LaunchedEffect(messageComposerContext.composerMode) {
|
||||
when (val modeValue = messageComposerContext.composerMode) {
|
||||
is MessageComposerMode.Edit ->
|
||||
richTextEditorState.setHtml(modeValue.defaultContent)
|
||||
if (showTextFormatting) {
|
||||
richTextEditorState.setHtml(modeValue.defaultContent)
|
||||
} else {
|
||||
markdownTextEditorState.text.update(modeValue.defaultContent, true)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -188,7 +210,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val memberSuggestions = remember { mutableStateListOf<MentionSuggestion>() }
|
||||
val memberSuggestions = remember { mutableStateListOf<ResolvedMentionSuggestion>() }
|
||||
LaunchedEffect(isMentionsEnabled) {
|
||||
if (!isMentionsEnabled) return@LaunchedEffect
|
||||
val currentUserId = currentSessionIdHolder.current
|
||||
|
|
@ -229,22 +251,69 @@ class MessageComposerPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val textEditorState by rememberUpdatedState(
|
||||
if (showTextFormatting) {
|
||||
TextEditorState.Rich(richTextEditorState)
|
||||
} else {
|
||||
TextEditorState.Markdown(markdownTextEditorState)
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(showTextFormatting) {
|
||||
if (!applyFormattingModeChanges) {
|
||||
applyFormattingModeChanges = true
|
||||
return@LaunchedEffect
|
||||
}
|
||||
if (showTextFormatting) {
|
||||
val markdown = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
||||
richTextEditorState.setMarkdown(markdown)
|
||||
richTextEditorState.requestFocus()
|
||||
} else {
|
||||
val markdown = richTextEditorState.messageMarkdown
|
||||
markdownTextEditorState.text.update(markdown, true)
|
||||
// Give some time for the focus of the previous editor to be cleared
|
||||
delay(100)
|
||||
markdownTextEditorState.requestFocusAction()
|
||||
}
|
||||
}
|
||||
|
||||
val mentionSpanProvider = if (isTesting) {
|
||||
null
|
||||
} else {
|
||||
rememberMentionSpanProvider(
|
||||
currentUserId = room.sessionId,
|
||||
permalinkParser = permalinkParser,
|
||||
)
|
||||
}
|
||||
|
||||
fun handleEvents(event: MessageComposerEvents) {
|
||||
when (event) {
|
||||
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
|
||||
MessageComposerEvents.CloseSpecialMode -> {
|
||||
if (messageComposerContext.composerMode is MessageComposerMode.Edit) {
|
||||
localCoroutineScope.launch {
|
||||
richTextEditorState.setHtml("")
|
||||
textEditorState.reset()
|
||||
}
|
||||
}
|
||||
messageComposerContext.composerMode = MessageComposerMode.Normal
|
||||
}
|
||||
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(
|
||||
message = event.message,
|
||||
updateComposerMode = { messageComposerContext.composerMode = it },
|
||||
richTextEditorState = richTextEditorState,
|
||||
)
|
||||
is MessageComposerEvents.SendMessage -> {
|
||||
val html = if (showTextFormatting) {
|
||||
richTextEditorState.messageHtml
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val markdown = if (showTextFormatting) {
|
||||
richTextEditorState.messageMarkdown
|
||||
} else {
|
||||
markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
||||
}
|
||||
appCoroutineScope.sendMessage(
|
||||
message = Message(html = html, markdown = markdown),
|
||||
updateComposerMode = { messageComposerContext.composerMode = it },
|
||||
textEditorState = textEditorState,
|
||||
)
|
||||
}
|
||||
is MessageComposerEvents.SendUri -> appCoroutineScope.sendAttachment(
|
||||
attachment = Attachment.Media(
|
||||
localMedia = localMediaFactory.createFromUri(
|
||||
|
|
@ -335,15 +404,26 @@ class MessageComposerPresenter @Inject constructor(
|
|||
}
|
||||
is MessageComposerEvents.InsertMention -> {
|
||||
localCoroutineScope.launch {
|
||||
when (val mention = event.mention) {
|
||||
is MentionSuggestion.Room -> {
|
||||
richTextEditorState.insertAtRoomMentionAtSuggestion()
|
||||
if (showTextFormatting) {
|
||||
when (val mention = event.mention) {
|
||||
is ResolvedMentionSuggestion.AtRoom -> {
|
||||
richTextEditorState.insertAtRoomMentionAtSuggestion()
|
||||
}
|
||||
is ResolvedMentionSuggestion.Member -> {
|
||||
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
|
||||
val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
|
||||
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
||||
}
|
||||
}
|
||||
is MentionSuggestion.Member -> {
|
||||
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
|
||||
val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
|
||||
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
||||
} else if (markdownTextEditorState.currentMentionSuggestion != null) {
|
||||
mentionSpanProvider?.let {
|
||||
markdownTextEditorState.insertMention(
|
||||
mention = event.mention,
|
||||
mentionSpanProvider = it,
|
||||
permalinkBuilder = permalinkBuilder,
|
||||
)
|
||||
}
|
||||
suggestionSearchTrigger.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -351,7 +431,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
return MessageComposerState(
|
||||
richTextEditorState = richTextEditorState,
|
||||
textEditorState = textEditorState,
|
||||
permalinkParser = permalinkParser,
|
||||
isFullScreen = isFullScreen.value,
|
||||
mode = messageComposerContext.composerMode,
|
||||
|
|
@ -369,21 +449,26 @@ class MessageComposerPresenter @Inject constructor(
|
|||
private fun CoroutineScope.sendMessage(
|
||||
message: Message,
|
||||
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
|
||||
richTextEditorState: RichTextEditorState,
|
||||
textEditorState: TextEditorState,
|
||||
) = launch {
|
||||
val capturedMode = messageComposerContext.composerMode
|
||||
val mentions = richTextEditorState.mentionsState?.let { state ->
|
||||
buildList {
|
||||
if (state.hasAtRoomMention) {
|
||||
add(Mention.AtRoom)
|
||||
}
|
||||
for (userId in state.userIds) {
|
||||
add(Mention.User(UserId(userId)))
|
||||
}
|
||||
val mentions = when (textEditorState) {
|
||||
is TextEditorState.Rich -> {
|
||||
textEditorState.richTextEditorState.mentionsState?.let { state ->
|
||||
buildList {
|
||||
if (state.hasAtRoomMention) {
|
||||
add(Mention.AtRoom)
|
||||
}
|
||||
for (userId in state.userIds) {
|
||||
add(Mention.User(UserId(userId)))
|
||||
}
|
||||
}
|
||||
}.orEmpty()
|
||||
}
|
||||
}.orEmpty()
|
||||
is TextEditorState.Markdown -> textEditorState.state.getMentions()
|
||||
}
|
||||
// Reset composer right away
|
||||
richTextEditorState.setHtml("")
|
||||
textEditorState.reset()
|
||||
updateComposerMode(MessageComposerMode.Normal)
|
||||
when (capturedMode) {
|
||||
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html, mentions = mentions)
|
||||
|
|
|
|||
|
|
@ -19,16 +19,16 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Stable
|
||||
data class MessageComposerState(
|
||||
val richTextEditorState: RichTextEditorState,
|
||||
val textEditorState: TextEditorState,
|
||||
val permalinkParser: PermalinkParser,
|
||||
val isFullScreen: Boolean,
|
||||
val mode: MessageComposerMode,
|
||||
|
|
@ -37,12 +37,10 @@ data class MessageComposerState(
|
|||
val canShareLocation: Boolean,
|
||||
val canCreatePoll: Boolean,
|
||||
val attachmentsState: AttachmentsState,
|
||||
val memberSuggestions: ImmutableList<MentionSuggestion>,
|
||||
val memberSuggestions: ImmutableList<ResolvedMentionSuggestion>,
|
||||
val currentUserId: UserId,
|
||||
val eventSink: (MessageComposerEvents) -> Unit,
|
||||
) {
|
||||
val hasFocus: Boolean = richTextEditorState.hasFocus
|
||||
}
|
||||
)
|
||||
|
||||
@Immutable
|
||||
sealed interface AttachmentsState {
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@
|
|||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.textcomposer.aRichTextEditorState
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider<MessageCompos
|
|||
}
|
||||
|
||||
fun aMessageComposerState(
|
||||
richTextEditorState: RichTextEditorState = aRichTextEditorState(),
|
||||
textEditorState: TextEditorState = TextEditorState.Rich(aRichTextEditorState()),
|
||||
isFullScreen: Boolean = false,
|
||||
mode: MessageComposerMode = MessageComposerMode.Normal,
|
||||
showTextFormatting: Boolean = false,
|
||||
|
|
@ -43,9 +43,9 @@ fun aMessageComposerState(
|
|||
canShareLocation: Boolean = true,
|
||||
canCreatePoll: Boolean = true,
|
||||
attachmentsState: AttachmentsState = AttachmentsState.None,
|
||||
memberSuggestions: ImmutableList<MentionSuggestion> = persistentListOf(),
|
||||
memberSuggestions: ImmutableList<ResolvedMentionSuggestion> = persistentListOf(),
|
||||
) = MessageComposerState(
|
||||
richTextEditorState = richTextEditorState,
|
||||
textEditorState = textEditorState,
|
||||
permalinkParser = object : PermalinkParser {
|
||||
override fun parse(uriString: String): PermalinkData = TODO()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMe
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.textcomposer.TextComposer
|
||||
import io.element.android.libraries.textcomposer.model.Message
|
||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
|
|
@ -44,13 +43,12 @@ internal fun MessageComposerView(
|
|||
state: MessageComposerState,
|
||||
voiceMessageState: VoiceMessageComposerState,
|
||||
subcomposing: Boolean,
|
||||
enableTextFormatting: Boolean,
|
||||
enableVoiceMessages: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val view = LocalView.current
|
||||
fun sendMessage(message: Message) {
|
||||
state.eventSink(MessageComposerEvents.SendMessage(message))
|
||||
fun sendMessage() {
|
||||
state.eventSink(MessageComposerEvents.SendMessage)
|
||||
}
|
||||
|
||||
fun sendUri(uri: Uri) {
|
||||
|
|
@ -85,7 +83,7 @@ internal fun MessageComposerView(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
fun onRequestFocus() {
|
||||
coroutineScope.launch {
|
||||
state.richTextEditorState.requestFocus()
|
||||
state.textEditorState.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +105,7 @@ internal fun MessageComposerView(
|
|||
|
||||
TextComposer(
|
||||
modifier = modifier,
|
||||
state = state.richTextEditorState,
|
||||
state = state.textEditorState,
|
||||
voiceMessageState = voiceMessageState.voiceMessageState,
|
||||
permalinkParser = state.permalinkParser,
|
||||
subcomposing = subcomposing,
|
||||
|
|
@ -118,7 +116,6 @@ internal fun MessageComposerView(
|
|||
onResetComposerMode = ::onCloseSpecialMode,
|
||||
onAddAttachment = ::onAddAttachment,
|
||||
onDismissTextFormatting = ::onDismissTextFormatting,
|
||||
enableTextFormatting = enableTextFormatting,
|
||||
enableVoiceMessages = enableVoiceMessages,
|
||||
onVoiceRecorderEvent = onVoiceRecorderEvent,
|
||||
onVoicePlayerEvent = onVoicePlayerEvent,
|
||||
|
|
@ -142,7 +139,6 @@ internal fun MessageComposerViewPreview(
|
|||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
state = state,
|
||||
voiceMessageState = aVoiceMessageComposerState(),
|
||||
enableTextFormatting = true,
|
||||
enableVoiceMessages = true,
|
||||
subcomposing = false,
|
||||
)
|
||||
|
|
@ -150,7 +146,6 @@ internal fun MessageComposerViewPreview(
|
|||
modifier = Modifier.height(200.dp),
|
||||
state = state,
|
||||
voiceMessageState = aVoiceMessageComposerState(),
|
||||
enableTextFormatting = true,
|
||||
enableVoiceMessages = true,
|
||||
subcomposing = false,
|
||||
)
|
||||
|
|
@ -167,7 +162,6 @@ internal fun MessageComposerViewVoicePreview(
|
|||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
state = aMessageComposerState(),
|
||||
voiceMessageState = state,
|
||||
enableTextFormatting = true,
|
||||
enableVoiceMessages = true,
|
||||
subcomposing = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ import javax.inject.Inject
|
|||
|
||||
interface RichTextEditorStateFactory {
|
||||
@Composable
|
||||
fun create(): RichTextEditorState
|
||||
fun remember(): RichTextEditorState
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRichTextEditorStateFactory @Inject constructor() : RichTextEditorStateFactory {
|
||||
@Composable
|
||||
override fun create(): RichTextEditorState {
|
||||
override fun remember(): RichTextEditorState {
|
||||
return rememberRichTextEditorState()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,12 +268,16 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
|||
}
|
||||
// Find and set as URLSpans any links present in the text
|
||||
LinkifyCompat.addLinks(this, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
|
||||
// Restore old spans if they don't conflict with the new ones
|
||||
// Restore old spans, remove new ones if there is a conflict
|
||||
for ((urlSpan, location) in oldURLSpans) {
|
||||
val (start, end) = location
|
||||
if (getSpans<URLSpan>(start, end).isEmpty()) {
|
||||
setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
val addedSpans = getSpans<URLSpan>(start, end).orEmpty()
|
||||
if (addedSpans.isNotEmpty()) {
|
||||
for (span in addedSpans) {
|
||||
removeSpan(span)
|
||||
}
|
||||
}
|
||||
setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="emoji_picker_category_activity">"აქტივობები"</string>
|
||||
<string name="emoji_picker_category_flags">"დროშები"</string>
|
||||
<string name="emoji_picker_category_foods">"Საჭმელ-სასმელი"</string>
|
||||
<string name="emoji_picker_category_nature">"ცხოველები & ბუნება"</string>
|
||||
<string name="emoji_picker_category_objects">"ობიექტები"</string>
|
||||
<string name="emoji_picker_category_people">"ღიმილები & ხალხი"</string>
|
||||
<string name="emoji_picker_category_places">"მოგზაურობა და ადგილები"</string>
|
||||
<string name="emoji_picker_category_symbols">"სიმბოლოები"</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_invite_again_alert_message">"გსურთ მათი კვლავ მოწვევა?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"თქვენ მარტო ხართ ამ ჩატში"</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_less_reactions">"ნაკლების ჩვენება"</string>
|
||||
<string name="screen_room_timeline_message_copied">"შეტყობინება დაკოპირდა"</string>
|
||||
<string name="screen_room_timeline_no_permission_to_post">"თქვენ არ გაქვთ ამ ოთახში გამოქვეყნების ნებართვა"</string>
|
||||
<string name="screen_room_timeline_reactions_show_less">"ნაკლების ჩვენება"</string>
|
||||
<string name="screen_room_timeline_reactions_show_more">"მეტის ჩვენება"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"ახალი"</string>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$dოთახის ცვლილება"</item>
|
||||
<item quantity="other">"%1$dოთახის ცვლილებები"</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
@ -49,6 +49,7 @@
|
|||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="one">"%1$s tastează"</item>
|
||||
<item quantity="few">"%1$s tastează"</item>
|
||||
<item quantity="other">"%1$s tastează"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_typing_two_members">"%1$s și %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -538,7 +538,7 @@ class MessagesPresenterTest {
|
|||
// Initially the composer doesn't have focus, so we don't show the alert
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
// When the input field is focused we show the alert
|
||||
initialState.composerState.richTextEditorState.requestFocus()
|
||||
initialState.composerState.textEditorState.requestFocus()
|
||||
val focusedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state ->
|
||||
state.showReinvitePrompt
|
||||
}.last()
|
||||
|
|
@ -561,7 +561,7 @@ class MessagesPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
initialState.composerState.richTextEditorState.requestFocus()
|
||||
initialState.composerState.textEditorState.requestFocus()
|
||||
val focusedState = awaitItem()
|
||||
assertThat(focusedState.showReinvitePrompt).isFalse()
|
||||
}
|
||||
|
|
@ -576,7 +576,7 @@ class MessagesPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
initialState.composerState.richTextEditorState.requestFocus()
|
||||
initialState.composerState.textEditorState.requestFocus()
|
||||
val focusedState = awaitItem()
|
||||
assertThat(focusedState.showReinvitePrompt).isFalse()
|
||||
}
|
||||
|
|
@ -781,7 +781,7 @@ class MessagesPresenterTest {
|
|||
): MessagesPresenter {
|
||||
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
|
||||
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
||||
val appPreferencesStore = InMemoryAppPreferencesStore(isRichTextEditorEnabled = true)
|
||||
val appPreferencesStore = InMemoryAppPreferencesStore()
|
||||
val sessionPreferencesStore = InMemorySessionPreferencesStore()
|
||||
val messageComposerPresenter = MessageComposerPresenter(
|
||||
appCoroutineScope = this,
|
||||
|
|
@ -800,7 +800,10 @@ class MessagesPresenterTest {
|
|||
permalinkParser = FakePermalinkParser(),
|
||||
permalinkBuilder = FakePermalinkBuilder(),
|
||||
timelineController = TimelineController(matrixRoom),
|
||||
)
|
||||
).apply {
|
||||
showTextFormatting = true
|
||||
isTesting = true
|
||||
}
|
||||
val voiceMessageComposerPresenter = VoiceMessageComposerPresenter(
|
||||
this,
|
||||
FakeVoiceRecorder(),
|
||||
|
|
@ -853,7 +856,6 @@ class MessagesPresenterTest {
|
|||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||
navigator = navigator,
|
||||
clipboardHelper = clipboardHelper,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
featureFlagsService = FakeFeatureFlagService(),
|
||||
buildMeta = aBuildMeta(),
|
||||
dispatchers = coroutineDispatchers,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ 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.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
|
|
@ -77,10 +76,11 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
|
|||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.libraries.textcomposer.model.Message
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||
import io.element.android.libraries.textcomposer.model.SuggestionType
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
|
|
@ -127,7 +127,7 @@ class MessageComposerPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.isFullScreen).isFalse()
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(initialState.textEditorState.messageHtml()).isEqualTo("")
|
||||
assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal)
|
||||
assertThat(initialState.showAttachmentSourcePicker).isFalse()
|
||||
assertThat(initialState.canShareLocation).isTrue()
|
||||
|
|
@ -158,10 +158,10 @@ class MessageComposerPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.richTextEditorState.setHtml(A_MESSAGE)
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
initialState.richTextEditorState.setHtml("")
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
initialState.textEditorState.setHtml(A_MESSAGE)
|
||||
assertThat(initialState.textEditorState.messageHtml()).isEqualTo(A_MESSAGE)
|
||||
initialState.textEditorState.setHtml("")
|
||||
assertThat(initialState.textEditorState.messageHtml()).isEqualTo("")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ class MessageComposerPresenterTest {
|
|||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val state = presenter.present()
|
||||
remember(state, state.richTextEditorState.messageHtml) { state }
|
||||
remember(state, state.textEditorState.messageHtml()) { state }
|
||||
}.test {
|
||||
var state = awaitFirstItem()
|
||||
val mode = anEditMode()
|
||||
|
|
@ -178,11 +178,11 @@ class MessageComposerPresenterTest {
|
|||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
state = awaitItem()
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
assertThat(state.textEditorState.messageHtml()).isEqualTo(A_MESSAGE)
|
||||
state = backToNormalMode(state, skipCount = 1)
|
||||
|
||||
// The message that was being edited is cleared
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(state.textEditorState.messageHtml()).isEqualTo("")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ class MessageComposerPresenterTest {
|
|||
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(state.textEditorState.messageHtml()).isEqualTo("")
|
||||
backToNormalMode(state)
|
||||
}
|
||||
}
|
||||
|
|
@ -213,11 +213,11 @@ class MessageComposerPresenterTest {
|
|||
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
state.richTextEditorState.setHtml(A_REPLY)
|
||||
state.textEditorState.setHtml(A_REPLY)
|
||||
state = backToNormalMode(state)
|
||||
|
||||
// The message typed while replying is not cleared
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo(A_REPLY)
|
||||
assertThat(state.textEditorState.messageHtml()).isEqualTo(A_REPLY)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -232,25 +232,54 @@ class MessageComposerPresenterTest {
|
|||
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(state.textEditorState.messageHtml()).isEqualTo("")
|
||||
backToNormalMode(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - send message`() = runTest {
|
||||
fun `present - send message with rich text enabled`() = runTest {
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val state = presenter.present()
|
||||
remember(state, state.richTextEditorState.messageHtml) { state }
|
||||
remember(state, state.textEditorState.messageHtml()) { state }
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.richTextEditorState.setHtml(A_MESSAGE)
|
||||
initialState.textEditorState.setHtml(A_MESSAGE)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
|
||||
assertThat(withMessageState.textEditorState.messageHtml()).isEqualTo(A_MESSAGE)
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(messageSentState.textEditorState.messageHtml()).isEqualTo("")
|
||||
waitForPredicate { analyticsService.capturedEvents.size == 1 }
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
Composer(
|
||||
inThread = false,
|
||||
isEditing = false,
|
||||
isReply = false,
|
||||
messageType = Composer.MessageType.Text,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - send message with plain text enabled`() = runTest {
|
||||
val permalinkBuilder = FakePermalinkBuilder(result = { Result.success("") })
|
||||
val presenter = createPresenter(this, isRichTextEditorEnabled = false)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val state = presenter.present()
|
||||
val messageMarkdown = state.textEditorState.messageMarkdown(permalinkBuilder)
|
||||
remember(state, messageMarkdown) { state }
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.textEditorState.setMarkdown(A_MESSAGE)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.textEditorState.messageHtml()).isNull()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.textEditorState.messageMarkdown(permalinkBuilder)).isEqualTo("")
|
||||
waitForPredicate { analyticsService.capturedEvents.size == 1 }
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
Composer(
|
||||
|
|
@ -278,23 +307,23 @@ class MessageComposerPresenterTest {
|
|||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val state = presenter.present()
|
||||
remember(state, state.richTextEditorState.messageHtml) { state }
|
||||
remember(state, state.textEditorState.messageHtml()) { state }
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(initialState.textEditorState.messageHtml()).isEqualTo("")
|
||||
val mode = anEditMode()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
skipItems(1)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.mode).isEqualTo(mode)
|
||||
assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE)
|
||||
assertThat(withMessageState.textEditorState.messageHtml()).isEqualTo(A_MESSAGE)
|
||||
withMessageState.textEditorState.setHtml(ANOTHER_MESSAGE)
|
||||
val withEditedMessageState = awaitItem()
|
||||
assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE.toMessage()))
|
||||
assertThat(withEditedMessageState.textEditorState.messageHtml()).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage)
|
||||
skipItems(1)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(messageSentState.textEditorState.messageHtml()).isEqualTo("")
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
@ -328,23 +357,23 @@ class MessageComposerPresenterTest {
|
|||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val state = presenter.present()
|
||||
remember(state, state.richTextEditorState.messageHtml) { state }
|
||||
remember(state, state.textEditorState.messageHtml()) { state }
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(initialState.textEditorState.messageHtml()).isEqualTo("")
|
||||
val mode = anEditMode(eventId = null, transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
skipItems(1)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.mode).isEqualTo(mode)
|
||||
assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE)
|
||||
assertThat(withMessageState.textEditorState.messageHtml()).isEqualTo(A_MESSAGE)
|
||||
withMessageState.textEditorState.setHtml(ANOTHER_MESSAGE)
|
||||
val withEditedMessageState = awaitItem()
|
||||
assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE.toMessage()))
|
||||
assertThat(withEditedMessageState.textEditorState.messageHtml()).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage)
|
||||
skipItems(1)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(messageSentState.textEditorState.messageHtml()).isEqualTo("")
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
@ -380,17 +409,17 @@ class MessageComposerPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(initialState.textEditorState.messageHtml()).isEqualTo("")
|
||||
val mode = aReplyMode()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
val state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo("")
|
||||
state.richTextEditorState.setHtml(A_REPLY)
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo(A_REPLY)
|
||||
state.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage()))
|
||||
assertThat(state.textEditorState.messageHtml()).isEqualTo("")
|
||||
state.textEditorState.setHtml(A_REPLY)
|
||||
assertThat(state.textEditorState.messageHtml()).isEqualTo(A_REPLY)
|
||||
state.eventSink.invoke(MessageComposerEvents.SendMessage)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(messageSentState.textEditorState.messageHtml()).isEqualTo("")
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
@ -725,7 +754,7 @@ class MessageComposerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - ToggleTextFormatting toggles text formatting`() = runTest {
|
||||
val presenter = createPresenter(this)
|
||||
val presenter = createPresenter(this, isRichTextEditorEnabled = false)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -735,11 +764,12 @@ class MessageComposerPresenterTest {
|
|||
val composerOptions = awaitItem()
|
||||
assertThat(composerOptions.showAttachmentSourcePicker).isTrue()
|
||||
composerOptions.eventSink(MessageComposerEvents.ToggleTextFormatting(true))
|
||||
awaitItem() // composer options closed
|
||||
skipItems(2) // composer options closed
|
||||
val showTextFormatting = awaitItem()
|
||||
assertThat(showTextFormatting.showAttachmentSourcePicker).isFalse()
|
||||
assertThat(showTextFormatting.showTextFormatting).isTrue()
|
||||
showTextFormatting.eventSink(MessageComposerEvents.ToggleTextFormatting(false))
|
||||
skipItems(1)
|
||||
val finished = awaitItem()
|
||||
assertThat(finished.showTextFormatting).isFalse()
|
||||
}
|
||||
|
|
@ -781,19 +811,19 @@ class MessageComposerPresenterTest {
|
|||
// An empty suggestion returns the room and joined members that are not the current user
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
||||
assertThat(awaitItem().memberSuggestions)
|
||||
.containsExactly(MentionSuggestion.Room, MentionSuggestion.Member(bob), MentionSuggestion.Member(david))
|
||||
.containsExactly(ResolvedMentionSuggestion.AtRoom, ResolvedMentionSuggestion.Member(bob), ResolvedMentionSuggestion.Member(david))
|
||||
|
||||
// A suggestion containing a part of "room" will also return the room mention
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "roo")))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(MentionSuggestion.Room)
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(ResolvedMentionSuggestion.AtRoom)
|
||||
|
||||
// A non-empty suggestion will return those joined members whose user id matches it
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "bob")))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(MentionSuggestion.Member(bob))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(ResolvedMentionSuggestion.Member(bob))
|
||||
|
||||
// A non-empty suggestion will return those joined members whose display name matches it
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "dave")))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(MentionSuggestion.Member(david))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(ResolvedMentionSuggestion.Member(david))
|
||||
|
||||
// If the suggestion isn't a mention, no suggestions are returned
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Command, "")))
|
||||
|
|
@ -803,7 +833,7 @@ class MessageComposerPresenterTest {
|
|||
room.givenCanTriggerRoomNotification(Result.success(false))
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
||||
assertThat(awaitItem().memberSuggestions)
|
||||
.containsExactly(MentionSuggestion.Member(bob), MentionSuggestion.Member(david))
|
||||
.containsExactly(ResolvedMentionSuggestion.Member(bob), ResolvedMentionSuggestion.Member(david))
|
||||
|
||||
// If room is a DM, `RoomMemberSuggestion.Room` is not returned
|
||||
room.givenCanTriggerRoomNotification(Result.success(true))
|
||||
|
|
@ -844,7 +874,7 @@ class MessageComposerPresenterTest {
|
|||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().memberSuggestions)
|
||||
.containsExactly(MentionSuggestion.Member(bob), MentionSuggestion.Member(david))
|
||||
.containsExactly(ResolvedMentionSuggestion.Member(bob), ResolvedMentionSuggestion.Member(david))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -862,10 +892,10 @@ class MessageComposerPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.richTextEditorState.setHtml("Hey @bo")
|
||||
initialState.eventSink(MessageComposerEvents.InsertMention(MentionSuggestion.Member(aRoomMember(userId = A_USER_ID_2))))
|
||||
initialState.textEditorState.setHtml("Hey @bo")
|
||||
initialState.eventSink(MessageComposerEvents.InsertMention(ResolvedMentionSuggestion.Member(aRoomMember(userId = A_USER_ID_2))))
|
||||
|
||||
assertThat(initialState.richTextEditorState.messageHtml)
|
||||
assertThat(initialState.textEditorState.messageHtml())
|
||||
.isEqualTo("Hey <a href='https://matrix.to/#/${A_USER_ID_2.value}'>${A_USER_ID_2.value}</a>")
|
||||
}
|
||||
}
|
||||
|
|
@ -892,14 +922,14 @@ class MessageComposerPresenterTest {
|
|||
|
||||
// Check intentional mentions on message sent
|
||||
val mentionUser1 = listOf(A_USER_ID.value)
|
||||
initialState.richTextEditorState.mentionsState = MentionsState(
|
||||
(initialState.textEditorState as? TextEditorState.Rich)?.richTextEditorState?.mentionsState = MentionsState(
|
||||
userIds = mentionUser1,
|
||||
roomIds = emptyList(),
|
||||
roomAliases = emptyList(),
|
||||
hasAtRoomMention = false
|
||||
)
|
||||
initialState.richTextEditorState.setHtml(A_MESSAGE)
|
||||
initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
|
||||
initialState.textEditorState.setHtml(A_MESSAGE)
|
||||
initialState.eventSink(MessageComposerEvents.SendMessage)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
@ -908,14 +938,14 @@ class MessageComposerPresenterTest {
|
|||
// Check intentional mentions on reply sent
|
||||
initialState.eventSink(MessageComposerEvents.SetMode(aReplyMode()))
|
||||
val mentionUser2 = listOf(A_USER_ID_2.value)
|
||||
awaitItem().richTextEditorState.mentionsState = MentionsState(
|
||||
(awaitItem().textEditorState as? TextEditorState.Rich)?.richTextEditorState?.mentionsState = MentionsState(
|
||||
userIds = mentionUser2,
|
||||
roomIds = emptyList(),
|
||||
roomAliases = emptyList(),
|
||||
hasAtRoomMention = false
|
||||
)
|
||||
|
||||
initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
|
||||
initialState.eventSink(MessageComposerEvents.SendMessage)
|
||||
advanceUntilIdle()
|
||||
|
||||
assert(replyMessageLambda)
|
||||
|
|
@ -926,14 +956,14 @@ class MessageComposerPresenterTest {
|
|||
skipItems(1)
|
||||
initialState.eventSink(MessageComposerEvents.SetMode(anEditMode()))
|
||||
val mentionUser3 = listOf(A_USER_ID_3.value)
|
||||
awaitItem().richTextEditorState.mentionsState = MentionsState(
|
||||
(awaitItem().textEditorState as? TextEditorState.Rich)?.richTextEditorState?.mentionsState = MentionsState(
|
||||
userIds = mentionUser3,
|
||||
roomIds = emptyList(),
|
||||
roomAliases = emptyList(),
|
||||
hasAtRoomMention = false
|
||||
)
|
||||
|
||||
initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
|
||||
initialState.eventSink(MessageComposerEvents.SendMessage)
|
||||
advanceUntilIdle()
|
||||
|
||||
assert(editMessageLambda)
|
||||
|
|
@ -949,7 +979,7 @@ class MessageComposerPresenterTest {
|
|||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
val state = presenter.present()
|
||||
remember(state, state.richTextEditorState.messageHtml) { state }
|
||||
remember(state, state.textEditorState.messageHtml()) { state }
|
||||
}.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SendUri(Uri.parse("content://uri")))
|
||||
|
|
@ -1007,7 +1037,8 @@ class MessageComposerPresenterTest {
|
|||
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
||||
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
||||
permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder()
|
||||
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
|
||||
isRichTextEditorEnabled: Boolean = true,
|
||||
) = MessageComposerPresenter(
|
||||
coroutineScope,
|
||||
room,
|
||||
|
|
@ -1025,7 +1056,10 @@ class MessageComposerPresenterTest {
|
|||
permalinkParser = FakePermalinkParser(),
|
||||
permalinkBuilder = permalinkBuilder,
|
||||
timelineController = TimelineController(room),
|
||||
)
|
||||
).apply {
|
||||
isTesting = true
|
||||
showTextFormatting = isRichTextEditorEnabled
|
||||
}
|
||||
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
// Skip 2 item if Mentions feature is enabled, else 1
|
||||
|
|
@ -1043,7 +1077,10 @@ fun anEditMode(
|
|||
fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, false, AN_EVENT_ID, A_MESSAGE)
|
||||
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)
|
||||
|
||||
private fun String.toMessage() = Message(
|
||||
html = this,
|
||||
markdown = this,
|
||||
)
|
||||
private suspend fun TextEditorState.setHtml(html: String) {
|
||||
(this as? TextEditorState.Rich)?.richTextEditorState?.setHtml(html) ?: error("TextEditorState is not Rich")
|
||||
}
|
||||
|
||||
private fun TextEditorState.setMarkdown(markdown: String) {
|
||||
(this as? TextEditorState.Markdown)?.state?.text?.update(markdown, needsDisplaying = false) ?: error("TextEditorState is not Markdown")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import io.element.android.wysiwyg.compose.rememberRichTextEditorState
|
|||
|
||||
class TestRichTextEditorStateFactory : RichTextEditorStateFactory {
|
||||
@Composable
|
||||
override fun create(): RichTextEditorState {
|
||||
override fun remember(): RichTextEditorState {
|
||||
return rememberRichTextEditorState("", fake = true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.factories.event
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.URLSpan
|
||||
import androidx.core.text.buildSpannedString
|
||||
|
|
@ -46,6 +48,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
|
|||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
|
|
@ -75,6 +78,7 @@ import org.robolectric.RobolectricTestRunner
|
|||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class TimelineItemContentMessageFactoryTest {
|
||||
@Test
|
||||
|
|
@ -641,6 +645,31 @@ class TimelineItemContentMessageFactoryTest {
|
|||
assertThat((result as TimelineItemEmoteContent).formattedBody).isEqualTo(SpannableString("* Bob formatted"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a message with existing URLSpans keeps it after linkification`() = runTest {
|
||||
val expectedSpanned = SpannableStringBuilder().apply {
|
||||
append("Test ")
|
||||
inSpans(URLSpan("https://www.example.org")) {
|
||||
append("me@matrix.org")
|
||||
}
|
||||
}
|
||||
val sut = createTimelineItemContentMessageFactory(
|
||||
htmlConverterTransform = { expectedSpanned },
|
||||
permalinkParser = FakePermalinkParser { PermalinkData.FallbackLink(Uri.EMPTY) }
|
||||
)
|
||||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
type = TextMessageType(
|
||||
body = "Test [me@matrix.org](https://www.example.org)",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "Test <a href=\"https://www.example.org\">me@matrix.org</a>")
|
||||
)
|
||||
),
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expectedSpanned)
|
||||
}
|
||||
|
||||
private fun createMessageContent(
|
||||
body: String = "Body",
|
||||
inReplyTo: InReplyTo? = null,
|
||||
|
|
@ -660,12 +689,13 @@ class TimelineItemContentMessageFactoryTest {
|
|||
private fun createTimelineItemContentMessageFactory(
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
htmlConverterTransform: (String) -> CharSequence = { it },
|
||||
permalinkParser: FakePermalinkParser = FakePermalinkParser(),
|
||||
) = TimelineItemContentMessageFactory(
|
||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||
featureFlagService = featureFlagService,
|
||||
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform),
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
permalinkParser = permalinkParser,
|
||||
)
|
||||
|
||||
private fun createStickerContent(
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ class InReplyToDetailTest {
|
|||
senderProfile = aProfileTimelineDetails(),
|
||||
content = RoomMembershipContent(
|
||||
userId = A_USER_ID,
|
||||
userDisplayName = null,
|
||||
change = MembershipChange.INVITED,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ class InReplyToMetadataKtTest {
|
|||
fun `room membership content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetailsReady(
|
||||
eventContent = RoomMembershipContent(A_USER_ID, null)
|
||||
eventContent = RoomMembershipContent(A_USER_ID, null, null)
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
|
|||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
||||
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
|
||||
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
|
@ -34,12 +34,12 @@ class DefaultVoiceMessageMediaRepoTest {
|
|||
|
||||
@Test
|
||||
fun `cache miss - downloads and returns cached file successfully`() = runTest {
|
||||
val fakeMediaLoader = FakeMediaLoader().apply {
|
||||
val matrixMediaLoader = FakeMatrixMediaLoader().apply {
|
||||
path = temporaryFolder.createRustMediaFile().path
|
||||
}
|
||||
val repo = createDefaultVoiceMessageMediaRepo(
|
||||
temporaryFolder = temporaryFolder,
|
||||
matrixMediaLoader = fakeMediaLoader,
|
||||
matrixMediaLoader = matrixMediaLoader,
|
||||
)
|
||||
|
||||
repo.getMediaFile().let { result ->
|
||||
|
|
@ -53,12 +53,12 @@ class DefaultVoiceMessageMediaRepoTest {
|
|||
|
||||
@Test
|
||||
fun `cache miss - download fails`() = runTest {
|
||||
val fakeMediaLoader = FakeMediaLoader().apply {
|
||||
val matrixMediaLoader = FakeMatrixMediaLoader().apply {
|
||||
shouldFail = true
|
||||
}
|
||||
val repo = createDefaultVoiceMessageMediaRepo(
|
||||
temporaryFolder = temporaryFolder,
|
||||
matrixMediaLoader = fakeMediaLoader,
|
||||
matrixMediaLoader = matrixMediaLoader,
|
||||
)
|
||||
|
||||
repo.getMediaFile().let { result ->
|
||||
|
|
@ -71,7 +71,7 @@ class DefaultVoiceMessageMediaRepoTest {
|
|||
|
||||
@Test
|
||||
fun `cache miss - download succeeds but file move fails`() = runTest {
|
||||
val fakeMediaLoader = FakeMediaLoader().apply {
|
||||
val matrixMediaLoader = FakeMatrixMediaLoader().apply {
|
||||
path = temporaryFolder.createRustMediaFile().path
|
||||
}
|
||||
File(temporaryFolder.cachedFilePath).apply {
|
||||
|
|
@ -83,7 +83,7 @@ class DefaultVoiceMessageMediaRepoTest {
|
|||
}
|
||||
val repo = createDefaultVoiceMessageMediaRepo(
|
||||
temporaryFolder = temporaryFolder,
|
||||
matrixMediaLoader = fakeMediaLoader,
|
||||
matrixMediaLoader = matrixMediaLoader,
|
||||
)
|
||||
|
||||
repo.getMediaFile().let { result ->
|
||||
|
|
@ -100,12 +100,12 @@ class DefaultVoiceMessageMediaRepoTest {
|
|||
@Test
|
||||
fun `cache hit - returns cached file successfully`() = runTest {
|
||||
temporaryFolder.createCachedFile()
|
||||
val fakeMediaLoader = FakeMediaLoader().apply {
|
||||
val matrixMediaLoader = FakeMatrixMediaLoader().apply {
|
||||
shouldFail = true // so that if we hit the media loader it will crash
|
||||
}
|
||||
val repo = createDefaultVoiceMessageMediaRepo(
|
||||
temporaryFolder = temporaryFolder,
|
||||
matrixMediaLoader = fakeMediaLoader,
|
||||
matrixMediaLoader = matrixMediaLoader,
|
||||
)
|
||||
|
||||
repo.getMediaFile().let { result ->
|
||||
|
|
@ -135,7 +135,7 @@ class DefaultVoiceMessageMediaRepoTest {
|
|||
|
||||
private fun createDefaultVoiceMessageMediaRepo(
|
||||
temporaryFolder: TemporaryFolder,
|
||||
matrixMediaLoader: MatrixMediaLoader = FakeMediaLoader(),
|
||||
matrixMediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(),
|
||||
mxcUri: String = MXC_URI,
|
||||
) = DefaultVoiceMessageMediaRepo(
|
||||
cacheDir = temporaryFolder.root,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class MigrationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - no migration should occurs if ApplicationMigrationVersion is the last one`() = runTest {
|
||||
val migrations = (1..10).map { FakeMigration(it) }
|
||||
val migrations = (1..10).map { FakeAppMigration(it) }
|
||||
val store = InMemoryMigrationStore(migrations.maxOf { it.order })
|
||||
val presenter = createPresenter(
|
||||
migrationStore = store,
|
||||
|
|
@ -57,7 +57,7 @@ class MigrationPresenterTest {
|
|||
@Test
|
||||
fun `present - testing all migrations`() = runTest {
|
||||
val store = InMemoryMigrationStore(0)
|
||||
val migrations = (1..10).map { FakeMigration(it) }
|
||||
val migrations = (1..10).map { FakeAppMigration(it) }
|
||||
val presenter = createPresenter(
|
||||
migrationStore = store,
|
||||
migrations = migrations.toSet(),
|
||||
|
|
@ -81,13 +81,13 @@ class MigrationPresenterTest {
|
|||
|
||||
private fun createPresenter(
|
||||
migrationStore: MigrationStore = InMemoryMigrationStore(0),
|
||||
migrations: Set<AppMigration> = setOf(FakeMigration(1)),
|
||||
migrations: Set<AppMigration> = setOf(FakeAppMigration(1)),
|
||||
) = MigrationPresenter(
|
||||
migrationStore = migrationStore,
|
||||
migrations = migrations,
|
||||
)
|
||||
|
||||
private class FakeMigration(
|
||||
private class FakeAppMigration(
|
||||
override val order: Int,
|
||||
var migrateLambda: LambdaNoParamRecorder<Unit> = lambdaRecorder { -> },
|
||||
) : AppMigration {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.preferences.test.FakeSessionPreferenceStoreFactory
|
||||
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
|
|
@ -33,7 +33,7 @@ class AppMigration02Test {
|
|||
updateData(aSessionData())
|
||||
}
|
||||
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSessionVerificationSkipped = false)
|
||||
val sessionPreferencesStoreFactory = FakeSessionPreferenceStoreFactory(
|
||||
val sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(
|
||||
getLambda = lambdaRecorder { _, _, -> sessionPreferencesStore },
|
||||
)
|
||||
val migration = AppMigration02(sessionStore = sessionStore, sessionPreferenceStoreFactory = sessionPreferencesStoreFactory)
|
||||
|
|
|
|||
|
|
@ -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_onboarding_sign_in_manually">"ხელით შესვლა"</string>
|
||||
<string name="screen_onboarding_sign_in_with_qr_code">"შესვლა QR კოდით"</string>
|
||||
<string name="screen_onboarding_sign_up">"ანგარიშის შექმნა"</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">"იყავი შენს element-ში"</string>
|
||||
</resources>
|
||||
11
features/poll/impl/src/main/res/values-ka/translations.xml
Normal file
11
features/poll/impl/src/main/res/values-ka/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_create_poll_add_option_btn">"ვარიანტის დამატება"</string>
|
||||
<string name="screen_create_poll_anonymous_desc">"შედეგების ჩვენება მხოლოდ გამოკითხვის დასრულების შემდეგ"</string>
|
||||
<string name="screen_create_poll_anonymous_headline">"ხმების დამალვა"</string>
|
||||
<string name="screen_create_poll_answer_hint">"ვარიანტი %1$d"</string>
|
||||
<string name="screen_create_poll_question_desc">"კითხვა ან თემა"</string>
|
||||
<string name="screen_create_poll_question_hint">"რას ეხება გამოკითხვა?"</string>
|
||||
<string name="screen_create_poll_title">"გამოკითხვის შექმნა"</string>
|
||||
<string name="screen_edit_poll_title">"გამოკითხვის რედაქტირება"</string>
|
||||
</resources>
|
||||
|
|
@ -57,6 +57,7 @@ dependencies {
|
|||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.features.rageshake.api)
|
||||
implementation(projects.features.lockscreen.api)
|
||||
implementation(projects.features.analytics.api)
|
||||
|
|
@ -90,6 +91,7 @@ dependencies {
|
|||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.features.rageshake.impl)
|
||||
testImplementation(projects.libraries.indicator.impl)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.features.logout.impl)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
|
|
|
|||
|
|
@ -212,9 +212,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
createNode<EditUserProfileNode>(buildContext, listOf(inputs))
|
||||
}
|
||||
NavTarget.LockScreenSettings -> {
|
||||
lockScreenEntryPoint.nodeBuilder(this, buildContext)
|
||||
.target(LockScreenEntryPoint.Target.Settings)
|
||||
.build()
|
||||
lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Settings).build()
|
||||
}
|
||||
NavTarget.BlockedUsers -> {
|
||||
createNode<BlockedUsersNode>(buildContext)
|
||||
|
|
|
|||
|
|
@ -19,10 +19,12 @@ package io.element.android.features.preferences.impl.advanced
|
|||
import io.element.android.compound.theme.Theme
|
||||
|
||||
sealed interface AdvancedSettingsEvents {
|
||||
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||
data object ChangeTheme : AdvancedSettingsEvents
|
||||
data object CancelChangeTheme : AdvancedSettingsEvents
|
||||
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
||||
data object ChangePushProvider : AdvancedSettingsEvents
|
||||
data object CancelChangePushProvider : AdvancedSettingsEvents
|
||||
data class SetPushProvider(val index: Int) : AdvancedSettingsEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@
|
|||
package io.element.android.features.preferences.impl.advanced
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -27,20 +29,26 @@ import io.element.android.compound.theme.Theme
|
|||
import io.element.android.compound.theme.mapToTheme
|
||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class AdvancedSettingsPresenter @Inject constructor(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val pushService: PushService,
|
||||
) : Presenter<AdvancedSettingsState> {
|
||||
@Composable
|
||||
override fun present(): AdvancedSettingsState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val isRichTextEditorEnabled by appPreferencesStore
|
||||
.isRichTextEditorEnabledFlow()
|
||||
.collectAsState(initial = false)
|
||||
val isDeveloperModeEnabled by appPreferencesStore
|
||||
.isDeveloperModeEnabledFlow()
|
||||
.collectAsState(initial = false)
|
||||
|
|
@ -52,11 +60,64 @@ class AdvancedSettingsPresenter @Inject constructor(
|
|||
}
|
||||
.collectAsState(initial = Theme.System)
|
||||
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// List of PushProvider -> Distributor
|
||||
val distributors = remember {
|
||||
pushService.getAvailablePushProviders()
|
||||
.flatMap { pushProvider ->
|
||||
pushProvider.getDistributors().map { distributor ->
|
||||
pushProvider to distributor
|
||||
}
|
||||
}
|
||||
}
|
||||
// List of Distributor names
|
||||
val distributorNames = remember {
|
||||
distributors.map { it.second.name }
|
||||
}
|
||||
|
||||
var currentDistributorName by remember { mutableStateOf<AsyncAction<String>>(AsyncAction.Uninitialized) }
|
||||
var refreshPushProvider by remember { mutableIntStateOf(0) }
|
||||
|
||||
LaunchedEffect(refreshPushProvider) {
|
||||
val p = pushService.getCurrentPushProvider()
|
||||
val name = p?.getCurrentDistributor(matrixClient)?.name
|
||||
currentDistributorName = if (name != null) {
|
||||
AsyncAction.Success(name)
|
||||
} else {
|
||||
AsyncAction.Failure(Exception("Failed to get current push provider"))
|
||||
}
|
||||
}
|
||||
|
||||
var showChangePushProviderDialog by remember { mutableStateOf(false) }
|
||||
|
||||
fun CoroutineScope.changePushProvider(
|
||||
data: Pair<PushProvider, Distributor>?
|
||||
) = launch {
|
||||
showChangePushProviderDialog = false
|
||||
data ?: return@launch
|
||||
// No op if the value is the same.
|
||||
if (data.second.name == currentDistributorName.dataOrNull()) return@launch
|
||||
currentDistributorName = AsyncAction.Loading
|
||||
data.let { (pushProvider, distributor) ->
|
||||
pushService.registerWith(
|
||||
matrixClient = matrixClient,
|
||||
pushProvider = pushProvider,
|
||||
distributor = distributor
|
||||
)
|
||||
.fold(
|
||||
{
|
||||
currentDistributorName = AsyncAction.Success(distributor.name)
|
||||
refreshPushProvider++
|
||||
},
|
||||
{
|
||||
currentDistributorName = AsyncAction.Failure(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: AdvancedSettingsEvents) {
|
||||
when (event) {
|
||||
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
|
||||
appPreferencesStore.setRichTextEditorEnabled(event.enabled)
|
||||
}
|
||||
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
|
||||
appPreferencesStore.setDeveloperModeEnabled(event.enabled)
|
||||
}
|
||||
|
|
@ -69,15 +130,20 @@ class AdvancedSettingsPresenter @Inject constructor(
|
|||
appPreferencesStore.setTheme(event.theme.name)
|
||||
showChangeThemeDialog = false
|
||||
}
|
||||
AdvancedSettingsEvents.ChangePushProvider -> showChangePushProviderDialog = true
|
||||
AdvancedSettingsEvents.CancelChangePushProvider -> showChangePushProviderDialog = false
|
||||
is AdvancedSettingsEvents.SetPushProvider -> localCoroutineScope.changePushProvider(distributors.getOrNull(event.index))
|
||||
}
|
||||
}
|
||||
|
||||
return AdvancedSettingsState(
|
||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSharePresenceEnabled = isSharePresenceEnabled,
|
||||
theme = theme,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
currentPushDistributor = currentDistributorName,
|
||||
availablePushDistributors = distributorNames.toImmutableList(),
|
||||
showChangePushProviderDialog = showChangePushProviderDialog,
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,16 @@
|
|||
package io.element.android.features.preferences.impl.advanced
|
||||
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class AdvancedSettingsState(
|
||||
val isRichTextEditorEnabled: Boolean,
|
||||
val isDeveloperModeEnabled: Boolean,
|
||||
val isSharePresenceEnabled: Boolean,
|
||||
val theme: Theme,
|
||||
val showChangeThemeDialog: Boolean,
|
||||
val currentPushDistributor: AsyncAction<String>,
|
||||
val availablePushDistributors: ImmutableList<String>,
|
||||
val showChangePushProviderDialog: Boolean,
|
||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,28 +18,37 @@ package io.element.android.features.preferences.impl.advanced
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
|
||||
override val values: Sequence<AdvancedSettingsState>
|
||||
get() = sequenceOf(
|
||||
aAdvancedSettingsState(),
|
||||
aAdvancedSettingsState(isRichTextEditorEnabled = true),
|
||||
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
||||
aAdvancedSettingsState(showChangeThemeDialog = true),
|
||||
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
|
||||
aAdvancedSettingsState(showChangePushProviderDialog = true),
|
||||
aAdvancedSettingsState(currentPushDistributor = AsyncAction.Loading),
|
||||
aAdvancedSettingsState(currentPushDistributor = AsyncAction.Failure(Exception("Failed to change distributor"))),
|
||||
)
|
||||
}
|
||||
|
||||
fun aAdvancedSettingsState(
|
||||
isRichTextEditorEnabled: Boolean = false,
|
||||
isDeveloperModeEnabled: Boolean = false,
|
||||
isSendPublicReadReceiptsEnabled: Boolean = false,
|
||||
showChangeThemeDialog: Boolean = false,
|
||||
currentPushDistributor: AsyncAction<String> = AsyncAction.Success("Firebase"),
|
||||
availablePushDistributors: List<String> = listOf("Firebase", "ntfy"),
|
||||
showChangePushProviderDialog: Boolean = false,
|
||||
eventSink: (AdvancedSettingsEvents) -> Unit = {},
|
||||
) = AdvancedSettingsState(
|
||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSharePresenceEnabled = isSendPublicReadReceiptsEnabled,
|
||||
theme = Theme.System,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = {}
|
||||
currentPushDistributor = currentPushDistributor,
|
||||
availablePushDistributors = availablePushDistributors.toImmutableList(),
|
||||
showChangePushProviderDialog = showChangePushProviderDialog,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue