Merge remote-tracking branch 'origin/develop' into feature/fga/pinned_message_banner_ui

This commit is contained in:
ganfra 2024-07-25 17:59:04 +02:00
commit 56e1957d3d
193 changed files with 4546 additions and 1696 deletions

View file

@ -1,3 +1,80 @@
Changes in Element X v0.5.0 (2024-07-24)
=========================================
### 🙌 Improvements
* Add icon for "Mark as read" and "Mark as unread" actions. by @bmarty in https://github.com/element-hq/element-x-android/pull/3144
* Add support for Picture In Picture for Element Call by @bmarty in https://github.com/element-hq/element-x-android/pull/3159
* Set pin grace period to 2 minutes by @bmarty in https://github.com/element-hq/element-x-android/pull/3172
* Unify the way we decide whether a room is a DM or a group room by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3100
* Subscribe to `RoomListItems` in the visible range by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3169
* Improve pip and add feature flag. by @bmarty in https://github.com/element-hq/element-x-android/pull/3199
* Open Source licenses: add color for links. by @bmarty in https://github.com/element-hq/element-x-android/pull/3215
* Cancel ringing call notification on call cancellation by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3047
### 🐛 Bugfixes
* Fix `MainActionButton` layout for long texts by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3158
* Always follow the desired theme for Pin, Incoming Call and Element Call screens by @bmarty in https://github.com/element-hq/element-x-android/pull/3165
* Fix empty screen issue after clearing the cache by @bmarty in https://github.com/element-hq/element-x-android/pull/3163
* Restore intentional mentions in the markdown/plain text editor by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3193
* Fix crash in the room list after a forced log out in background by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3180
* Clear existing notification when a room is marked as read by @bmarty in https://github.com/element-hq/element-x-android/pull/3203
* Fix crash when Pin code screen is displayed by @bmarty in https://github.com/element-hq/element-x-android/pull/3205
* Fix pillification not working for non formatted message bodies by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3201
* Update grammar on Matrix Ids to be more spec compliant and render error instead of infinite loading in room member list screen by @bmarty in https://github.com/element-hq/element-x-android/pull/3206
* Reduce the risk of text truncation in buttons. by @bmarty in https://github.com/element-hq/element-x-android/pull/3209
* Ensure that the manual dark theme is rendering correctly regarding -night resource and keyboard by @bmarty in https://github.com/element-hq/element-x-android/pull/3216
* Fix rendering issue of SunsetPage in dark mode by @bmarty in https://github.com/element-hq/element-x-android/pull/3217
* Fix linkification not working for `Spanned` strings in text messages by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3233
* Edit : fallback to room.edit when timeline item is not found. by @ganfra in https://github.com/element-hq/element-x-android/pull/3239
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3156
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3192
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3232
### 🧱 Build
* Remove Showkase processor not found warning from Danger by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3148
* Set targetSDK to 34 by @bmarty in https://github.com/element-hq/element-x-android/pull/3149
* Add a local copy of `inplace-fix.py` and `fix-pg-map-id.py` by @bmarty in https://github.com/element-hq/element-x-android/pull/3167
* Only add private SSH keys and clone submodules in the original repo by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3225
* Fix CI for forks by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3226
### Dependency upgrades
* Update dependency io.element.android:compound-android to v0.0.7 by @renovate in https://github.com/element-hq/element-x-android/pull/3143
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.31 by @renovate in https://github.com/element-hq/element-x-android/pull/3145
* Update dependency com.squareup:kotlinpoet to v1.18.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3150
* Update dependency org.robolectric:robolectric to v4.13 by @renovate in https://github.com/element-hq/element-x-android/pull/3157
* Update plugin dependencycheck to v10.0.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3154
* Update wysiwyg to v2.37.5 by @renovate in https://github.com/element-hq/element-x-android/pull/3162
* Update plugin sonarqube to v5.1.0.4882 by @renovate in https://github.com/element-hq/element-x-android/pull/3139
* Update dependency org.jsoup:jsoup to v1.18.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3171
* Update dependency com.google.firebase:firebase-bom to v33.1.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3178
* Update telephoto to v0.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3191
* Update dependency com.google.truth:truth to v1.4.4 by @renovate in https://github.com/element-hq/element-x-android/pull/3187
* Update dependency com.squareup:kotlinpoet to v1.18.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3194
* Update dependency io.mockk:mockk to v1.13.12 by @renovate in https://github.com/element-hq/element-x-android/pull/3198
* Update dependency io.sentry:sentry-android to v7.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3200
* Update plugin dependencycheck to v10.0.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3204
* Update dependency gradle to v8.9 by @renovate in https://github.com/element-hq/element-x-android/pull/3177
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.32 by @renovate in https://github.com/element-hq/element-x-android/pull/3202
* Update coil to v2.7.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3210
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.33 by @renovate in https://github.com/element-hq/element-x-android/pull/3220
* Update wysiwyg to v2.37.7 by @renovate in https://github.com/element-hq/element-x-android/pull/3218
* Update telephoto to v0.12.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3230
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.34 by @renovate in https://github.com/element-hq/element-x-android/pull/3237
### Others
* Reduce delay when selecting room list filters by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3160
* Add `--alignment-preserved true` when signing APK for F-Droid. by @bmarty in https://github.com/element-hq/element-x-android/pull/3161
* Ensure that all callback plugins are invoked. by @bmarty in https://github.com/element-hq/element-x-android/pull/3146
* Add generated screen to show open source licenses in Gplay variant by @bmarty in https://github.com/element-hq/element-x-android/pull/3207
* Performance : improve time to open a room. by @ganfra in https://github.com/element-hq/element-x-android/pull/3186
* Add logging to help debug forced logout issues by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3208
* Use the right filename for log files so they're sorted in rageshakes by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3219
* Compose : add immutability to some Reaction classes by @ganfra in https://github.com/element-hq/element-x-android/pull/3224
* Fix stickers display text on room summary by @surakin in https://github.com/element-hq/element-x-android/pull/3221
* Rework FakeMatrixRoom so that it contains only lambdas. by @bmarty in https://github.com/element-hq/element-x-android/pull/3229
Changes in Element X v0.4.16 (2024-07-05)
=========================================

View file

@ -13,7 +13,9 @@
<locale android:name="in"/>
<locale android:name="it"/>
<locale android:name="ka"/>
<locale android:name="pl"/>
<locale android:name="pt"/>
<locale android:name="pt_BR"/>
<locale android:name="ro"/>
<locale android:name="ru"/>
<locale android:name="sk"/>

View file

@ -123,7 +123,9 @@ class JoinRoomLoadedFlowNodeTest {
@Test
fun `given a room flow node when initialized then it loads messages entry point`() = runTest {
// GIVEN
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
updateMembersResult = { }
)
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
val roomFlowNode = createJoinedRoomLoadedFlowNode(
@ -144,7 +146,9 @@ class JoinRoomLoadedFlowNodeTest {
@Test
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
// GIVEN
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
updateMembersResult = { }
)
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())

View file

@ -33,7 +33,6 @@ import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class) class SendQueuesTest {
private val matrixClient = FakeMatrixClient()
private val room = FakeMatrixRoom()
private val networkMonitor = FakeNetworkMonitor()
private val sut = SendQueues(matrixClient, networkMonitor)
@ -43,11 +42,11 @@ import org.junit.Test
val setAllSendQueuesEnabledLambda = lambdaRecorder { _: Boolean -> }
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
matrixClient.givenGetRoomResult(room.roomId, room)
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
room.setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
val room = FakeMatrixRoom(
setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
)
matrixClient.givenGetRoomResult(room.roomId, room)
sut.launchIn(backgroundScope)
sendQueueDisabledFlow.emit(room.roomId)
@ -72,10 +71,11 @@ import org.junit.Test
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
networkMonitor.connectivity.value = NetworkStatus.Offline
matrixClient.givenGetRoomResult(room.roomId, room)
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
room.setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
val room = FakeMatrixRoom(
setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
)
matrixClient.givenGetRoomResult(room.roomId, room)
sut.launchIn(backgroundScope)

View file

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

View file

@ -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">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
<string name="screen_analytics_settings_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
</resources>

View file

@ -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">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_settings_read_terms">"Você pode ler todos os nossos termos %1$s ."</string>
<string name="screen_analytics_settings_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_settings_share_data">"Compartilhar dados de utilização"</string>
</resources>

View file

@ -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">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
<string name="screen_analytics_prompt_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Nie będziemy udostępniać Twoich danych podmiotom trzecim"</string>
<string name="screen_analytics_prompt_title">"Pomóż nam ulepszyć %1$s"</string>
</resources>

View file

@ -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">"Não registraremos nem criaremos perfil baseado em qualquer dado pessoal"</string>
<string name="screen_analytics_prompt_help_us_improve">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_prompt_read_terms">"Você pode ler todos os nossos termos %1$s ."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_prompt_settings">"Você pode desativar isso a qualquer momento"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Não compartilharemos seus dados com terceiros"</string>
<string name="screen_analytics_prompt_title">"Ajude a melhorar o %1$s"</string>
</resources>

View file

@ -3,4 +3,5 @@
<string name="call_foreground_service_channel_title_android">"Laufender Anruf"</string>
<string name="call_foreground_service_message_android">"Tippen, um zum Anruf zurückzukehren"</string>
<string name="call_foreground_service_title_android">"☎️ Anruf läuft"</string>
<string name="screen_incoming_call_subtitle_android">"Eingehender Element Call"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Połączenie w trakcie"</string>
<string name="call_foreground_service_message_android">"Stuknij, aby wrócić do rozmowy"</string>
<string name="call_foreground_service_title_android">"☎️ Rozmowa w toku"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Chamada em andamento"</string>
<string name="call_foreground_service_message_android">"Toque para retornar à chamada"</string>
<string name="call_foreground_service_title_android">"☎️ Chamada em andamento"</string>
</resources>

View file

@ -54,9 +54,9 @@ class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - fails if it can't generate the URL for the widget`() = runTest {
val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.failure(Exception("Can't generate URL for widget")))
}
val room = FakeMatrixRoom(
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.failure(Exception("Can't generate URL for widget")) }
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
@ -66,10 +66,10 @@ class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - fails if it can't get the widget driver`() = runTest {
val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
givenGetWidgetDriverResult(Result.failure(Exception("Can't get a widget driver")))
}
val room = FakeMatrixRoom(
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
getWidgetDriverResult = { Result.failure(Exception("Can't get a widget driver")) }
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
@ -79,10 +79,10 @@ class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
}
val room = FakeMatrixRoom(
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
@ -92,10 +92,10 @@ class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - will use a custom base url if it exists`() = runTest {
val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
}
val room = FakeMatrixRoom(
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
@ -120,10 +120,10 @@ class DefaultCallWidgetProviderTest {
val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { matrixClient ->
providesLambda(matrixClient)
}
val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
}
val room = FakeMatrixRoom(
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Nowy pokój"</string>
<string name="screen_create_room_add_people_title">"Zaproś znajomych"</string>
<string name="screen_create_room_error_creating_room">"Wystąpił błąd podczas tworzenia pokoju"</string>
<string name="screen_create_room_private_option_description">"Wiadomości w tym pokoju są szyfrowane. Szyfrowania nie można później wyłączyć."</string>
<string name="screen_create_room_private_option_title">"Pokój prywatny (tylko zaproszenie)"</string>
<string name="screen_create_room_public_option_description">"Wiadomości nie są szyfrowane i każdy może je odczytać. Możesz aktywować szyfrowanie później."</string>
<string name="screen_create_room_public_option_title">"Pokój publiczny (każdy)"</string>
<string name="screen_create_room_room_name_label">"Nazwa pokoju"</string>
<string name="screen_create_room_title">"Utwórz pokój"</string>
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>
<string name="screen_start_chat_error_starting_chat">"Wystąpił błąd podczas próby rozpoczęcia czatu"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Nova sala"</string>
<string name="screen_create_room_add_people_title">"Convidar pessoas"</string>
<string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string>
<string name="screen_create_room_private_option_description">"As mensagens nesta sala serão criptografadas. A criptografia não pode ser desativada posteriormente."</string>
<string name="screen_create_room_private_option_title">"Sala privativa (somente por convite)"</string>
<string name="screen_create_room_public_option_description">"As mensagens não serão criptografadas e qualquer pessoa pode lê-las. Você pode ativar a criptografia posteriormente."</string>
<string name="screen_create_room_public_option_title">"Sala pública (qualquer pessoa)"</string>
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
<string name="screen_create_room_title">"Criar uma sala"</string>
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string>
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar um chat"</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Możesz zmienić ustawienia później."</string>
<string name="screen_notification_optin_title">"Zezwól na powiadomienia i nie przegap żadnej wiadomości"</string>
<string name="screen_welcome_bullet_1">"Połączenia, ankiety, wyszukiwanie i inne zostaną dodane później w tym roku."</string>
<string name="screen_welcome_bullet_2">"Historia wiadomości dla pokoi szyfrowanych nie jest jeszcze dostępna."</string>
<string name="screen_welcome_bullet_3">"Chętnie poznamy Twoją opinię. Daj nam znać, co myślisz na stronie ustawień."</string>
<string name="screen_welcome_button">"Naprzód!"</string>
<string name="screen_welcome_subtitle">"Oto, co musisz wiedzieć:"</string>
<string name="screen_welcome_title">"Witamy w %1$s!"</string>
</resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Você pode alterar suas configurações mais tarde."</string>
<string name="screen_notification_optin_title">"Permita notificações e nunca perca uma mensagem"</string>
<string name="screen_welcome_bullet_1">"Chamadas, enquetes, pesquisa e muito mais serão adicionadas ainda este ano."</string>
<string name="screen_welcome_bullet_2">"O histórico de mensagens para salas criptografadas ainda não está disponível."</string>
<string name="screen_welcome_bullet_3">"Adoraríamos ouvir sua opinião. Deixe-nos saber o que você pensa através da página de configurações."</string>
<string name="screen_welcome_button">"Vamos lá!"</string>
<string name="screen_welcome_subtitle">"Aqui está o que você precisa saber:"</string>
<string name="screen_welcome_title">"Bem-vindo ao %1$s!"</string>
</resources>

View file

@ -33,6 +33,7 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.architecture.runUpdatingState
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.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import kotlinx.coroutines.CoroutineScope
@ -107,7 +108,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
) = launch {
acceptedAction.runUpdatingState {
joinRoom(
roomId = roomId,
roomIdOrAlias = roomId.toRoomIdOrAlias(),
serverNames = emptyList(),
trigger = JoinedRoom.Trigger.Invite,
)

View file

@ -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">"Czy na pewno chcesz odrzucić zaproszenie do dołączenia do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Odrzuć czat"</string>
<string name="screen_invites_empty_list">"Brak zaproszeń"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) zaprosił Cię"</string>
</resources>

View file

@ -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">"Tem certeza de que deseja recusar o convite para ingressar em %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Recusar convite"</string>
<string name="screen_invites_decline_direct_chat_message">"Tem certeza de que deseja recusar esse chat privado com %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Recusar chat"</string>
<string name="screen_invites_empty_list">"Sem convites"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s) convidou você"</string>
</resources>

View file

@ -23,7 +23,9 @@ import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction
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.SessionId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -92,9 +94,9 @@ class AcceptDeclineInvitePresenterTest {
val client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
result = FakeMatrixRoom().apply {
result = FakeMatrixRoom(
leaveRoomLambda = declineInviteFailure
}
)
)
}
val presenter = createAcceptDeclineInvitePresenter(client = client)
@ -142,9 +144,9 @@ class AcceptDeclineInvitePresenterTest {
val client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
result = FakeMatrixRoom().apply {
result = FakeMatrixRoom(
leaveRoomLambda = declineInviteSuccess
}
)
)
}
val presenter = createAcceptDeclineInvitePresenter(
@ -178,8 +180,8 @@ class AcceptDeclineInvitePresenterTest {
@Test
fun `present - accepting invite error flow`() = runTest {
val joinRoomFailure = lambdaRecorder { roomId: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
Result.failure<Unit>(RuntimeException("Failed to join room $roomId"))
val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.failure<Unit>(RuntimeException("Failed to join room $roomIdOrAlias"))
}
val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomFailure)
presenter.test {
@ -208,7 +210,7 @@ class AcceptDeclineInvitePresenterTest {
assert(joinRoomFailure)
.isCalledOnce()
.with(
value(A_ROOM_ID),
value(A_ROOM_ID.toRoomIdOrAlias()),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
)
@ -222,7 +224,7 @@ class AcceptDeclineInvitePresenterTest {
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
val joinRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.success(Unit)
}
val presenter = createAcceptDeclineInvitePresenter(
@ -248,7 +250,7 @@ class AcceptDeclineInvitePresenterTest {
assert(joinRoomSuccess)
.isCalledOnce()
.with(
value(A_ROOM_ID),
value(A_ROOM_ID.toRoomIdOrAlias()),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
)
@ -271,7 +273,7 @@ class AcceptDeclineInvitePresenterTest {
private fun createAcceptDeclineInvitePresenter(
client: MatrixClient = FakeMatrixClient(),
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
},
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),

View file

@ -96,7 +96,7 @@ class JoinRoomPresenter @AssistedInject constructor(
}
else -> {
value = ContentState.Loading(roomIdOrAlias)
val result = matrixClient.getRoomPreviewFromRoomId(roomId, serverNames)
val result = matrixClient.getRoomPreview(roomIdOrAlias, serverNames)
value = result.fold(
onSuccess = { roomPreview ->
roomPreview.toContentState()
@ -153,7 +153,7 @@ class JoinRoomPresenter @AssistedInject constructor(
private fun CoroutineScope.joinRoom(joinAction: MutableState<AsyncAction<Unit>>) = launch {
joinAction.runUpdatingState {
joinRoom.invoke(
roomId = roomId,
roomIdOrAlias = roomIdOrAlias,
serverNames = serverNames,
trigger = trigger
)

View file

@ -37,7 +37,11 @@ data class JoinRoomState(
val eventSink: (JoinRoomEvents) -> Unit
) {
val joinAuthorisationStatus = when (contentState) {
// Use the join authorisation status from the loaded content state
is ContentState.Loaded -> contentState.joinAuthorisationStatus
// Assume that if the room is unknown, the user can join it
is ContentState.UnknownRoom -> JoinAuthorisationStatus.CanJoin
// Otherwise assume that the user can't join the room
else -> JoinAuthorisationStatus.Unknown
}
}

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias
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.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
@ -180,7 +181,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is joined with success, all the parameters are provided`() = runTest {
val aTrigger = JoinedRoom.Trigger.MobilePermalink
val joinRoomLambda = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
val joinRoomLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.success(Unit)
}
val presenter = createJoinRoomPresenter(
@ -201,7 +202,7 @@ class JoinRoomPresenterTest {
}
joinRoomLambda.assertions()
.isCalledOnce()
.with(value(A_ROOM_ID), value(A_SERVER_LIST), value(aTrigger))
.with(value(A_ROOM_ID.toRoomIdOrAlias()), value(A_SERVER_LIST), value(aTrigger))
}
}
@ -366,7 +367,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is not known RoomPreview is loaded`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewFromRoomIdResult = { _, _ ->
getRoomPreviewResult = { _, _ ->
Result.success(
RoomPreview(
roomId = A_ROOM_ID,
@ -411,7 +412,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewFromRoomIdResult = { _, _ ->
getRoomPreviewResult = { _, _ ->
Result.failure(AN_EXCEPTION)
}
)
@ -449,7 +450,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewFromRoomIdResult = { _, _ ->
getRoomPreviewResult = { _, _ ->
Result.failure(Exception("403"))
}
)
@ -474,7 +475,7 @@ class JoinRoomPresenterTest {
serverNames: List<String> = emptyList(),
trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite,
matrixClient: MatrixClient = FakeMatrixClient(),
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
},
knockRoom: KnockRoom = FakeKnockRoom(),

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Czy na pewno chcesz opuścić tę konwersację? Konwersacja nie jest publiczna i nie będziesz mógł dołączyć ponownie bez zaproszenia."</string>
<string name="leave_room_alert_empty_subtitle">"Jesteś pewien, że chcesz opuścić ten pokój? Jesteś tu jedyną osobą. Jeśli wyjdziesz, nikt nie będzie mógł dołączyć, w tym Ty."</string>
<string name="leave_room_alert_private_subtitle">"Czy na pewno chcesz opuścić ten pokój? Ten pokój nie jest publiczny i nie będziesz mógł do niego wrócić bez zaproszenia."</string>
<string name="leave_room_alert_subtitle">"Jesteś pewien, że chcesz wyjść z tego pokoju?"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_room_alert_empty_subtitle">"Tem certeza de que deseja sair desta sala? Você é a única pessoa aqui. Se você sair, ninguém poderá ingressar no futuro, inclusive você."</string>
<string name="leave_room_alert_private_subtitle">"Tem certeza de que deseja sair desta sala? Esta sala não é pública e você não poderá entrar novamente sem um convite."</string>
<string name="leave_room_alert_subtitle">"Tem certeza de que deseja sair da sala?"</string>
</resources>

View file

@ -140,7 +140,9 @@ class DefaultLeaveRoomPresenterTest {
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
result = FakeMatrixRoom(),
result = FakeMatrixRoom(
leaveRoomLambda = { Result.success(Unit) }
),
)
},
roomMembershipObserver = roomMembershipObserver
@ -162,9 +164,9 @@ class DefaultLeaveRoomPresenterTest {
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
result = FakeMatrixRoom().apply {
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
},
result = FakeMatrixRoom(
leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
),
)
}
)
@ -186,7 +188,9 @@ class DefaultLeaveRoomPresenterTest {
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
result = FakeMatrixRoom(),
result = FakeMatrixRoom(
leaveRoomLambda = { Result.success(Unit) }
),
)
}
)
@ -208,9 +212,9 @@ class DefaultLeaveRoomPresenterTest {
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
result = FakeMatrixRoom().apply {
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
},
result = FakeMatrixRoom(
leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
),
)
}
)

View file

@ -29,13 +29,15 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
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.MatrixRoom
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.SendLocationInvocation
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@ -46,16 +48,18 @@ class SendLocationPresenterTest {
val warmUpRule = WarmUpRule()
private val fakePermissionsPresenter = FakePermissionsPresenter()
private val fakeMatrixRoom = FakeMatrixRoom()
private val fakeAnalyticsService = FakeAnalyticsService()
private val fakeMessageComposerContext = FakeMessageComposerContext()
private val fakeLocationActions = FakeLocationActions()
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
private fun createSendLocationPresenter(
matrixRoom: MatrixRoom = FakeMatrixRoom(),
): SendLocationPresenter = SendLocationPresenter(
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
},
room = fakeMatrixRoom,
room = matrixRoom,
analyticsService = fakeAnalyticsService,
messageComposerContext = fakeMessageComposerContext,
locationActions = fakeLocationActions,
@ -64,6 +68,7 @@ class SendLocationPresenterTest {
@Test
fun `initial state with permissions granted`() = runTest {
val sendLocationPresenter = createSendLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
@ -90,6 +95,7 @@ class SendLocationPresenterTest {
@Test
fun `initial state with permissions partially granted`() = runTest {
val sendLocationPresenter = createSendLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.SomeGranted,
@ -116,6 +122,7 @@ class SendLocationPresenterTest {
@Test
fun `initial state with permissions denied`() = runTest {
val sendLocationPresenter = createSendLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
@ -142,6 +149,7 @@ class SendLocationPresenterTest {
@Test
fun `initial state with permissions denied once`() = runTest {
val sendLocationPresenter = createSendLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
@ -168,6 +176,7 @@ class SendLocationPresenterTest {
@Test
fun `rationale dialog dismiss`() = runTest {
val sendLocationPresenter = createSendLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
@ -199,6 +208,7 @@ class SendLocationPresenterTest {
@Test
fun `rationale dialog continue`() = runTest {
val sendLocationPresenter = createSendLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
@ -227,6 +237,7 @@ class SendLocationPresenterTest {
@Test
fun `permission denied dialog dismiss`() = runTest {
val sendLocationPresenter = createSendLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
@ -258,6 +269,13 @@ class SendLocationPresenterTest {
@Test
fun `share sender location`() = runTest {
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, Result<Unit>> { _, _, _, _, _ ->
Result.success(Unit)
}
val matrixRoom = FakeMatrixRoom(
sendLocationResult = sendLocationResult,
)
val sendLocationPresenter = createSendLocationPresenter(matrixRoom)
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
@ -289,16 +307,14 @@ class SendLocationPresenterTest {
delay(1) // Wait for the coroutine to finish
assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
SendLocationInvocation(
body = "Location was shared at geo:3.0,4.0;u=5.0",
geoUri = "geo:3.0,4.0;u=5.0",
description = null,
zoomLevel = 15,
assetType = AssetType.SENDER
sendLocationResult.assertions().isCalledOnce()
.with(
value("Location was shared at geo:3.0,4.0;u=5.0"),
value("geo:3.0,4.0;u=5.0"),
value(null),
value(15),
value(AssetType.SENDER),
)
)
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1)
assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo(
@ -314,6 +330,13 @@ class SendLocationPresenterTest {
@Test
fun `share pin location`() = runTest {
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, Result<Unit>> { _, _, _, _, _ ->
Result.success(Unit)
}
val matrixRoom = FakeMatrixRoom(
sendLocationResult = sendLocationResult,
)
val sendLocationPresenter = createSendLocationPresenter(matrixRoom)
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
@ -345,16 +368,14 @@ class SendLocationPresenterTest {
delay(1) // Wait for the coroutine to finish
assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
SendLocationInvocation(
body = "Location was shared at geo:0.0,1.0",
geoUri = "geo:0.0,1.0",
description = null,
zoomLevel = 15,
assetType = AssetType.PIN
sendLocationResult.assertions().isCalledOnce()
.with(
value("Location was shared at geo:0.0,1.0"),
value("geo:0.0,1.0"),
value(null),
value(15),
value(AssetType.PIN),
)
)
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1)
assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo(
@ -370,6 +391,13 @@ class SendLocationPresenterTest {
@Test
fun `composer context passes through analytics`() = runTest {
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, Result<Unit>> { _, _, _, _, _ ->
Result.success(Unit)
}
val matrixRoom = FakeMatrixRoom(
sendLocationResult = sendLocationResult,
)
val sendLocationPresenter = createSendLocationPresenter(matrixRoom)
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
@ -418,6 +446,7 @@ class SendLocationPresenterTest {
@Test
fun `open settings activity`() = runTest {
val sendLocationPresenter = createSendLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
@ -452,6 +481,7 @@ class SendLocationPresenterTest {
@Test
fun `application name is in state`() = runTest {
val sendLocationPresenter = createSendLocationPresenter()
moleculeFlow(RecompositionMode.Immediate) {
sendLocationPresenter.present()
}.test {

View file

@ -41,6 +41,7 @@ dependencies {
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.cryptography.api)
implementation(projects.libraries.preferences.api)
implementation(projects.features.logout.api)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.services.appnavstate.api)
@ -59,4 +60,5 @@ dependencies {
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.features.logout.test)
}

View file

@ -29,7 +29,7 @@ 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.features.logout.api.LogoutUseCase
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -41,7 +41,7 @@ import javax.inject.Inject
class PinUnlockPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager,
private val biometricUnlockManager: BiometricUnlockManager,
private val signOut: SignOut,
private val logoutUseCase: LogoutUseCase,
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 {
signOut()
logoutUseCase.logout(ignoreSdkError = true)
}.runCatchingUpdatingState(signOutAction)
}
}

View file

@ -18,9 +18,9 @@ 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
import io.element.android.libraries.di.SessionScope
@ContributesTo(AppScope::class)
@ContributesTo(SessionScope::class)
interface PinUnlockBindings {
fun inject(activity: PinUnlockActivity)
}

View file

@ -1,40 +0,0 @@
/*
* 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")
}
}
}

View file

@ -1,12 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"ბიომეტრიული ავტორიზაცია"</string>
<string name="screen_app_lock_biometric_unlock">"ბიომეტრიული განბლოკვა"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"განბლოკვა ბიომეტრიით"</string>
<string name="screen_app_lock_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_biometric_unlock_allow_title">"%1$s დაშვება"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"მირჩევნია PIN-ის გამოყენება"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"დაზოგეთ დრო და გამოიყენეთ %1$s აპლიკაციის განსაბლოკად."</string>
<string name="screen_app_lock_setup_choose_pin">"აირჩიეთ PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"დაადასტურეთ PIN"</string>
<string name="screen_app_lock_setup_pin_context">"თქვენი ჩატების დამატებითი უსაფრთხოებისათვის დაბლოკეთ %1$s.
აირჩიეთ რაიმე ისეთი, რაც დაგამახსოვრდებათ. თუ დაგავიწყდებათ ეს PIN, აპლიკაციიდან გამოხვალთ."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"თქვენ არ შეგიძლიათ აირჩიოთ ეს PIN კოდი უსაფრთხოების მიზეზების გამო"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"აირჩიეთ სხვა PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"გთხოვთ შეიყვანოთ იგივე PIN ორჯერ"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-ები არ ემთხვევა"</string>
<string name="screen_app_lock_signout_alert_message">"გასაგრძელებლად საჭიროა ხელახლა შესვლა და ახალი PIN-ის შექმნა"</string>
<string name="screen_app_lock_signout_alert_title">"თქვენ ახლა გადიხართ…"</string>
<plurals name="screen_app_lock_subtitle">
@ -17,5 +31,7 @@
<item quantity="one">"არასწორი PIN. თქვენ %1$d შანსი დაგრჩათ"</item>
<item quantity="other">"არასწორი PIN. თქვენ %1$d შანსი დაგრჩათ"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"გამოიყენეთ ბიომეტრია"</string>
<string name="screen_app_lock_use_pin_android">"გამოიყენეთ PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"გასვლა…"</string>
</resources>

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"uwierzytelnienie biometryczne"</string>
<string name="screen_app_lock_biometric_unlock">"odblokowanie biometryczne"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Odblokuj za pomocą biometrii"</string>
<string name="screen_app_lock_forgot_pin">"Nie pamiętasz kodu PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Zmień kod PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Zezwól na uwierzytelnienie biometryczne"</string>
<string name="screen_app_lock_settings_remove_pin">"Usuń PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Czy na pewno chcesz usunąć PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Usunąć PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Zezwól na %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Wolę korzystać z kodu PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Zaoszczędź sobie trochę czasu i korzystaj z %1$s do odblokowywania aplikacji"</string>
<string name="screen_app_lock_setup_choose_pin">"Wybierz PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Potwierdź PIN"</string>
<string name="screen_app_lock_setup_pin_context">"Zablokuj %1$s, aby zwiększyć bezpieczeństwo swoich czatów.
Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz tego PINU, zostaniesz wylogowany z aplikacji."</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Nie możesz wybrać tego PINU ze względów bezpieczeństwa"</string>
<string name="screen_app_lock_setup_pin_forbidden_dialog_title">"Wybierz inny kod PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Wprowadź ten sam kod PIN dwa razy"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINY nie pasują do siebie"</string>
<string name="screen_app_lock_signout_alert_message">"Aby kontynuować, zaloguj się ponownie i utwórz nowy kod PIN"</string>
<string name="screen_app_lock_signout_alert_title">"Trwa wylogowywanie"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Masz %1$d próbę, żeby odblokować"</item>
<item quantity="few">"Masz %1$d próby, żeby odblokować"</item>
<item quantity="many">"Masz %1$d prób, żeby odblokować"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Błędny PIN. Pozostała %1$d próba"</item>
<item quantity="few">"Błędny PIN. Pozostały %1$d próby"</item>
<item quantity="many">"Błędny PIN. Pozostało %1$d prób"</item>
</plurals>
<string name="screen_app_lock_use_biometric_android">"Użyj biometrii"</string>
<string name="screen_app_lock_use_pin_android">"Użyj kodu PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Wylogowywanie…"</string>
</resources>

View file

@ -0,0 +1,22 @@
<?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">"Esqueceu o PIN?"</string>
<string name="screen_app_lock_settings_change_pin">"Mudar código de PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Permitir desbloqueio biométrico"</string>
<string name="screen_app_lock_settings_remove_pin">"Remover PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Tem certeza de que quer remover o PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Remover PIN?"</string>
<string name="screen_app_lock_setup_choose_pin">"Escolher PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Confirmar PIN"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"Os PINs não correspondem"</string>
<string name="screen_app_lock_signout_alert_title">"Você está sendo desconectado"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Você tem %1$d tentativa de debloqueio"</item>
<item quantity="other">"Você tem %1$d tentativas de debloqueio"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"PIN incorreto. Você tem mais %1$d chance"</item>
<item quantity="other">"PIN incorreto. Você tem mais %1$d chances"</item>
</plurals>
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
</resources>

View file

@ -1,61 +0,0 @@
/*
* 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()
}
}

View file

@ -28,7 +28,7 @@ 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.features.logout.test.FakeLogoutUseCase
import io.element.android.libraries.architecture.AsyncData
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -106,9 +106,9 @@ class PinUnlockPresenterTest {
@Test
fun `present - forgot pin flow`() = runTest {
val signOutLambda = lambdaRecorder<String?> { null }
val signOut = FakeSignOut(signOutLambda)
val presenter = createPinUnlockPresenter(this, signOut = signOut)
val signOutLambda = lambdaRecorder<Boolean, String> { "" }
val signOut = FakeLogoutUseCase(signOutLambda)
val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -135,7 +135,7 @@ class PinUnlockPresenterTest {
awaitItem().also { state ->
assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java)
}
assert(signOutLambda).isCalledOnce().withNoParameter()
assert(signOutLambda).isCalledOnce()
}
}
@ -147,7 +147,7 @@ class PinUnlockPresenterTest {
scope: CoroutineScope,
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
signOut: SignOut = FakeSignOut(),
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }),
): PinUnlockPresenter {
val pinCodeManager = aPinCodeManager().apply {
addCallback(callback)
@ -156,7 +156,7 @@ class PinUnlockPresenterTest {
return PinUnlockPresenter(
pinCodeManager = pinCodeManager,
biometricUnlockManager = biometricUnlockManager,
signOut = signOut,
logoutUseCase = logoutUseCase,
coroutineScope = scope,
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
)

View file

@ -27,6 +27,7 @@
<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_qr_code_login_invalid_scan_state_retry_button">"ხელახლა ცდა"</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>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Zmień dostawcę konta"</string>
<string name="screen_account_provider_form_hint">"Adres serwera domowego"</string>
<string name="screen_account_provider_form_notice">"Wprowadź wyszukiwane hasło lub adres domeny."</string>
<string name="screen_account_provider_form_subtitle">"Szukaj serwera firmowego, społeczności lub prywatnego."</string>
<string name="screen_account_provider_form_title">"Znajdź dostawcę konta"</string>
<string name="screen_account_provider_signin_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
<string name="screen_account_provider_signin_title">"Zamierzasz się zalogować %s"</string>
<string name="screen_account_provider_signup_subtitle">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
<string name="screen_account_provider_signup_title">"Zamierzasz założyć konto na %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org jest ogromnym i darmowym serwerem na publicznej sieci Matrix zapewniający bezpieczną i zdecentralizowaną komunikację zarządzaną przez Fundację Matrix.org."</string>
<string name="screen_change_account_provider_other">"Inne"</string>
<string name="screen_change_account_provider_subtitle">"Użyj innego dostawcy konta, takiego jak własny serwer lub konta służbowego."</string>
<string name="screen_change_account_provider_title">"Zmień dostawcę konta"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nie mogliśmy połączyć się z tym serwerem domowym. Sprawdź, czy adres URL serwera został wprowadzony poprawnie. Jeśli adres URL jest poprawny, skontaktuj się z administratorem serwera w celu uzyskania dalszej pomocy."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Ten serwer obecnie nie obsługuje technologii Sliding Sync."</string>
<string name="screen_change_server_form_header">"Adres URL serwera domowego"</string>
<string name="screen_change_server_form_notice">"Możesz połączyć się tylko z serwerem, który obsługuje technologię Sliding Sync. Administrator serwera domowego będzie musiał ją skonfigurować. %1$s"</string>
<string name="screen_change_server_subtitle">"Jaki jest adres Twojego serwera?"</string>
<string name="screen_change_server_title">"Wybierz swój serwer"</string>
<string name="screen_login_error_deactivated_account">"To konto zostało dezaktywowane."</string>
<string name="screen_login_error_invalid_credentials">"Nieprawidłowa nazwa użytkownika i/lub hasło"</string>
<string name="screen_login_error_invalid_user_id">"To nie jest prawidłowy identyfikator użytkownika. Oczekiwany format: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_unsupported_authentication">"Wybrany serwer domowy nie obsługuje uwierzytelniania hasłem, ani OIDC. Skontaktuj się z jego administratorem lub wybierz inny serwer domowy."</string>
<string name="screen_login_form_header">"Wprowadź swoje dane"</string>
<string name="screen_login_subtitle">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string>
<string name="screen_login_title">"Witaj ponownie!"</string>
<string name="screen_login_title_with_homeserver">"Zaloguj się do %1$s"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Spróbuj ponownie"</string>
<string name="screen_server_confirmation_change_server">"Zmień dostawcę konta"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Serwer prywatny dla pracowników Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."</string>
<string name="screen_server_confirmation_message_register">"Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."</string>
<string name="screen_server_confirmation_title_login">"Zamierzasz się zalogować do %1$s"</string>
<string name="screen_server_confirmation_title_register">"Zamierzasz utworzyć konto na %1$s"</string>
<string name="screen_waitlist_message">"Obecnie istnieje duże zapotrzebowanie na %1$s na %2$s. Wróć do aplikacji za kilka dni i spróbuj ponownie.
Dziękujemy za Twoją cierpliwość!"</string>
<string name="screen_waitlist_message_success">"Witamy w %1$s!"</string>
<string name="screen_waitlist_title">"Już prawie gotowe!"</string>
<string name="screen_waitlist_title_success">"Witamy!"</string>
</resources>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Alterar provedor da conta"</string>
<string name="screen_account_provider_form_hint">"Endereço do servidor"</string>
<string name="screen_account_provider_form_notice">"Insira um termo de pesquisa ou um endereço de domínio."</string>
<string name="screen_account_provider_form_subtitle">"Procure uma empresa, comunidade ou servidor privado."</string>
<string name="screen_account_provider_form_title">"Encontre um provedor de contas"</string>
<string name="screen_account_provider_signin_subtitle">"Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails."</string>
<string name="screen_account_provider_signin_title">"Você está prestes a entrar em %s"</string>
<string name="screen_account_provider_signup_subtitle">"Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails."</string>
<string name="screen_account_provider_signup_title">"Você está prestes a criar uma conta em %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"O Matrix.org é um grande servidor gratuito na rede pública Matrix para comunicação segura e descentralizada, administrado pela Fundação Matrix.org."</string>
<string name="screen_change_account_provider_other">"Outro"</string>
<string name="screen_change_account_provider_subtitle">"Use um provedor de conta diferente, como seu próprio servidor privado ou uma conta corporativa."</string>
<string name="screen_change_account_provider_title">"Alterar provedor da conta"</string>
<string name="screen_change_server_error_invalid_homeserver">"Não conseguimos acessar esse servidor. Verifique se você inseriu a URL do servidor corretamente. Se a URL estiver correta, entre em contato com o administrador do servidor para obter mais ajuda."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Este servidor atualmente não oferece suporte à tecnologia sliding sync."</string>
<string name="screen_change_server_form_header">"URL do servidor"</string>
<string name="screen_change_server_form_notice">"Você só pode se conectar a um servidor existente que ofereça suporte à tecnologia sliding sync. O administrador do seu servidor precisará configurá-lo. %1$s"</string>
<string name="screen_change_server_subtitle">"Qual é o endereço do seu servidor?"</string>
<string name="screen_change_server_title">"Selecione seu servidor"</string>
<string name="screen_login_error_deactivated_account">"Essa conta foi desativada."</string>
<string name="screen_login_error_invalid_credentials">"Nome de usuário e/ou senha incorretos"</string>
<string name="screen_login_error_invalid_user_id">"Esse não é um identificador de usuário válido. Formato esperado: \'@usuário:servidor.org\'"</string>
<string name="screen_login_error_unsupported_authentication">"O servidor selecionado não suporta senha ou login no OIDC. Entre em contato com o administrador ou escolha outro servidor."</string>
<string name="screen_login_form_header">"Insira seus dados"</string>
<string name="screen_login_subtitle">"A Matrix é uma rede aberta para comunicação segura e descentralizada."</string>
<string name="screen_login_title">"Bem-vindo de volta!"</string>
<string name="screen_login_title_with_homeserver">"Iniciar sessão em %1$s"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tente novamente"</string>
<string name="screen_server_confirmation_change_server">"Alterar provedor da conta"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Um servidor privado para funcionários do Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"A Matrix é uma rede aberta para comunicação segura e descentralizada."</string>
<string name="screen_server_confirmation_message_register">"Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails."</string>
<string name="screen_server_confirmation_title_login">"Você está prestes a fazer login em %1$s"</string>
<string name="screen_server_confirmation_title_register">"Você está prestes a criar uma conta em %1$s"</string>
<string name="screen_waitlist_message">"Há uma grande demanda por %1$s sobre %2$s no momento. Volte ao aplicativo em alguns dias e tente novamente.
Obrigado pela sua paciência!"</string>
<string name="screen_waitlist_message_success">"Bem-vindo ao %1$s!"</string>
<string name="screen_waitlist_title">"Você está quase lá."</string>
<string name="screen_waitlist_title_success">"Você está dentro."</string>
</resources>

View file

@ -0,0 +1,33 @@
/*
* 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
*
* https://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.logout.api
/**
* Used to trigger a log out of the current user from any part of the app.
*/
interface LogoutUseCase {
/**
* Log out the current user and then perform any needed cleanup tasks.
* @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway.
* @return the session id of the logged out user.
*/
suspend fun logout(ignoreSdkError: Boolean): String
interface Factory {
fun create(sessionId: String): LogoutUseCase
}
}

View file

@ -0,0 +1,43 @@
/*
* 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
*
* https://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.logout.impl
import com.squareup.anvil.annotations.ContributesBinding
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
class DefaultLogoutUseCase @AssistedInject constructor(
@Assisted private val sessionId: String,
private val matrixClientProvider: MatrixClientProvider,
) : LogoutUseCase {
@ContributesBinding(AppScope::class)
@AssistedFactory
interface Factory : LogoutUseCase.Factory {
override fun create(sessionId: String): DefaultLogoutUseCase
}
override suspend fun logout(ignoreSdkError: Boolean): String {
val matrixClient = matrixClientProvider.getOrRestore(SessionId(sessionId)).getOrThrow()
matrixClient.logout(ignoreSdkError = ignoreSdkError)
return sessionId
}
}

View file

@ -0,0 +1,36 @@
/*
* 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
*
* https://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.logout.impl
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
@Module
@ContributesTo(SessionScope::class)
object SessionLogoutModule {
@Provides
fun provideLogoutUseCase(
currentSessionIdHolder: CurrentSessionIdHolder,
factory: DefaultLogoutUseCase.Factory,
): LogoutUseCase {
return factory.create(currentSessionIdHolder.current.value)
}
}

View file

@ -4,5 +4,15 @@
<string name="screen_signout_confirmation_dialog_submit">"გამოსვლა"</string>
<string name="screen_signout_confirmation_dialog_title">"გამოსვლა"</string>
<string name="screen_signout_in_progress_dialog_content">"გასვლა…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"თქვენ აპირებთ გასვლას თქვენი ბოლო სესიიდან. თუ ახლა გამოხვალთ, დაკარგავთ წვდომას თქვენს დაშიფრულ შეტყობინებებზე."</string>
<string name="screen_signout_key_backup_disabled_title">"თქვენ გამორთეთ სარეზერვო ასლი"</string>
<string name="screen_signout_key_backup_offline_subtitle">"თქვენი გასაღებების სარეზერვო ასლის შექმნა მიმდინარეობდა იმ დროს, როდესაც გამოხვედით. დაკავშირდით ისევ ისე, რომ სარეზერვო ასლი შეიქმნას ანგარიშიდან გამოსვლის გარეშე."</string>
<string name="screen_signout_key_backup_offline_title">"თქვენი გასაღებების სარეზერვო ასლი ჯერ კიდევ შექმნის პროცესშია"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"გთხოვთ დაელოდეთ ამის დასრულებას სისტემიდან გამოსვლამდე."</string>
<string name="screen_signout_key_backup_ongoing_title">"თქვენი გასაღებების სარეზერვო ასლი ჯერ კიდევ შექმნის პროცესშია"</string>
<string name="screen_signout_preference_item">"გამოსვლა"</string>
<string name="screen_signout_recovery_disabled_subtitle">"თქვენ აპირებთ გასვლას თქვენი ბოლო სესიიდან. თუ ახლა გამოხვალთ, დაკარგავთ წვდომას თქვენს დაშიფრულ შეტყობინებებზე."</string>
<string name="screen_signout_recovery_disabled_title">"აღდგენა არ არის დაყენებული"</string>
<string name="screen_signout_save_recovery_key_subtitle">"თქვენ აპირებთ გასვლას თქვენი ბოლო სესიიდან. თუ ახლა გამოხვალთ, შესაძლოა დაკარგოთ წვდომა თქვენს დაშიფრულ შეტყობინებებზე."</string>
<string name="screen_signout_save_recovery_key_title">"შეინახეთ თქვენი აღდგენის გასაღები?"</string>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Czy na pewno chcesz się wylogować?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Wyloguj się"</string>
<string name="screen_signout_confirmation_dialog_title">"Wyloguj się"</string>
<string name="screen_signout_in_progress_dialog_content">"Wylogowywanie…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string>
<string name="screen_signout_key_backup_disabled_title">"Wyłączyłeś backup"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Twoje klucze były nadal archiwizowane po przejściu w tryb offline. Połącz się ponownie, aby zapisać w chmurze przed wylogowaniem."</string>
<string name="screen_signout_key_backup_offline_title">"Twoje klucze są nadal archiwizowane"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Zanim się wylogujesz, poczekaj na zakończenie operacji."</string>
<string name="screen_signout_key_backup_ongoing_title">"Twoje klucze są nadal archiwizowane"</string>
<string name="screen_signout_preference_item">"Wyloguj się"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string>
<string name="screen_signout_recovery_disabled_title">"Nie ustawiono przywracania"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."</string>
<string name="screen_signout_save_recovery_key_title">"Czy zapisałeś swój klucz przywracania?"</string>
</resources>

View file

@ -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">"Você tem certeza que deseja sair?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Sair"</string>
<string name="screen_signout_confirmation_dialog_title">"Sair"</string>
<string name="screen_signout_in_progress_dialog_content">"Saindo…"</string>
<string name="screen_signout_preference_item">"Sair"</string>
</resources>

View file

@ -5,7 +5,7 @@
* 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
* https://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,
@ -14,8 +14,16 @@
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock.signout
interface SignOut {
suspend operator fun invoke(): String?
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.logout.test"
}
dependencies {
implementation(libs.coroutines.core)
implementation(projects.tests.testutils)
api(projects.features.logout.api)
}

View file

@ -5,7 +5,7 @@
* 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
* https://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,
@ -14,15 +14,15 @@
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock
package io.element.android.features.logout.test
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
import io.element.android.tests.testutils.simulateLongTask
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.tests.testutils.lambda.lambdaError
class FakeSignOut(
var lambda: () -> String? = { null }
) : SignOut {
override suspend fun invoke(): String? = simulateLongTask {
lambda()
class FakeLogoutUseCase(
var logoutLambda: (Boolean) -> String = lambdaError()
) : LogoutUseCase {
override suspend fun logout(ignoreSdkError: Boolean): String {
return logoutLambda(ignoreSdkError)
}
}

View file

@ -60,6 +60,7 @@ import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.map
@ -436,7 +437,14 @@ class MessageComposerPresenter @Inject constructor(
val eventId = capturedMode.eventId
val transactionId = capturedMode.transactionId
timelineController.invokeOnCurrentTimeline {
// First try to edit the message in the current timeline
editMessage(eventId, transactionId, message.markdown, message.html, message.mentions)
.onFailure { cause ->
if (cause is TimelineException.EventNotFound && eventId != null) {
// if the event is not found in the timeline, try to edit the message directly
room.editMessage(eventId, message.markdown, message.html, message.mentions)
}
}
}
}

View file

@ -264,27 +264,27 @@ class TimelineItemContentMessageFactory @Inject constructor(
}
private fun CharSequence.withFixedURLSpans(): CharSequence {
if (this !is Spannable) return this
val spannable = this.toSpannable()
// Get all URL spans, as they will be removed by LinkifyCompat.addLinks
val oldURLSpans = getSpans<URLSpan>(0, length).associateWith {
val start = getSpanStart(it)
val end = getSpanEnd(it)
val oldURLSpans = spannable.getSpans<URLSpan>(0, length).associateWith {
val start = spannable.getSpanStart(it)
val end = spannable.getSpanEnd(it)
Pair(start, end)
}
// 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)
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
// Restore old spans, remove new ones if there is a conflict
for ((urlSpan, location) in oldURLSpans) {
val (start, end) = location
val addedSpans = getSpans<URLSpan>(start, end).orEmpty()
val addedSpans = spannable.getSpans<URLSpan>(start, end).orEmpty()
if (addedSpans.isNotEmpty()) {
for (span in addedSpans) {
removeSpan(span)
spannable.removeSpan(span)
}
}
setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return this
return spannable
}
}

View file

@ -21,8 +21,10 @@
<string name="screen_room_attachment_source_poll">"გამოკითხვა"</string>
<string name="screen_room_attachment_text_formatting">"ტექსტის ფორმატირება"</string>
<string name="screen_room_encrypted_history_banner">"შეტყობინებების ისტორია ამჟამად მიუწვდომელია."</string>
<string name="screen_room_encrypted_history_banner_unverified">"შეტყობინებების ისტორია ამ ოთახში მიუწვდომელია. დაადასტურეთ ეს მოწყობილობა თქვენი შეტყობინებების ისტორიის სანახავად."</string>
<string name="screen_room_invite_again_alert_message">"გსურთ მათი კვლავ მოწვევა?"</string>
<string name="screen_room_invite_again_alert_title">"თქვენ მარტო ხართ ამ ჩატში"</string>
<string name="screen_room_mentions_at_room_subtitle">"მთელი ოთახისათვის შეტყობინება"</string>
<string name="screen_room_mentions_at_room_title">"ყველა"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Ხელახლა გაგზავნა"</string>
<string name="screen_room_retry_send_menu_title">"თქვენი შეტყობინების გაგზავნა ვერ მოხერხდა"</string>

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Aktywności"</string>
<string name="emoji_picker_category_flags">"Flagi"</string>
<string name="emoji_picker_category_foods">"Jedzenie i napoje"</string>
<string name="emoji_picker_category_nature">"Zwierzęta i natura"</string>
<string name="emoji_picker_category_objects">"Obiekty"</string>
<string name="emoji_picker_category_people">"Buźki i osoby"</string>
<string name="emoji_picker_category_places">"Podróż i miejsca"</string>
<string name="emoji_picker_category_symbols">"Symbole"</string>
<string name="screen_report_content_block_user">"Zablokuj użytkownika"</string>
<string name="screen_report_content_block_user_hint">"Sprawdź, czy chcesz ukryć wszystkie bieżące i przyszłe wiadomości od tego użytkownika."</string>
<string name="screen_report_content_explanation">"Ta wiadomość zostanie zgłoszona do administratora Twojego serwera domowego. Nie będzie mógł on przeczytać żadnych zaszyfrowanych wiadomości."</string>
<string name="screen_report_content_hint">"Powód zgłoszenia treści"</string>
<string name="screen_room_attachment_source_camera">"Kamera"</string>
<string name="screen_room_attachment_source_camera_photo">"Zrób zdjęcie"</string>
<string name="screen_room_attachment_source_camera_video">"Nagraj film"</string>
<string name="screen_room_attachment_source_files">"Załącznik"</string>
<string name="screen_room_attachment_source_gallery">"Zdjęcia i filmy"</string>
<string name="screen_room_attachment_source_location">"Lokalizacja"</string>
<string name="screen_room_attachment_source_poll">"Ankieta"</string>
<string name="screen_room_attachment_text_formatting">"Formatowanie tekstu"</string>
<string name="screen_room_encrypted_history_banner">"Historia wiadomości jest obecnie niedostępna."</string>
<string name="screen_room_encrypted_history_banner_unverified">"Historia wiadomości jest niedostępna w tym pokoju. Zweryfikuj to urządzenie, aby zobaczyć historię wiadomości."</string>
<string name="screen_room_invite_again_alert_message">"Czy chcesz zaprosić ich z powrotem?"</string>
<string name="screen_room_invite_again_alert_title">"Jesteś sam na tym czacie"</string>
<string name="screen_room_mentions_at_room_subtitle">"Powiadom cały pokój"</string>
<string name="screen_room_mentions_at_room_title">"Wszyscy"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Wyślij ponownie"</string>
<string name="screen_room_retry_send_menu_title">"Nie udało się wysłać wiadomości"</string>
<string name="screen_room_timeline_add_reaction">"Dodaj emoji"</string>
<string name="screen_room_timeline_beginning_of_room">"To jest początek %1$s"</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"To jest początek tej konwersacji"</string>
<string name="screen_room_timeline_less_reactions">"Pokaż mniej"</string>
<string name="screen_room_timeline_message_copied">"Skopiowano wiadomość"</string>
<string name="screen_room_timeline_no_permission_to_post">"Nie masz uprawnień, aby pisać w tym pokoju"</string>
<string name="screen_room_timeline_reactions_show_less">"Pokaż mniej"</string>
<string name="screen_room_timeline_reactions_show_more">"Pokaż więcej"</string>
<string name="screen_room_timeline_read_marker_title">"Nowe"</string>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d zmiana pokoju"</item>
<item quantity="few">"%1$d zmian pokoju"</item>
<item quantity="many">"%1$d zmiany pokoju"</item>
</plurals>
<plurals name="screen_room_typing_notification">
<item quantity="one">"%1$s piszę"</item>
<item quantity="few">"%1$s piszą"</item>
<item quantity="many">"%1$s piszą"</item>
</plurals>
</resources>

View 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="emoji_picker_category_activity">"Atividades"</string>
<string name="emoji_picker_category_flags">"Bandeiras"</string>
<string name="emoji_picker_category_foods">"Comida &amp; Bebida"</string>
<string name="emoji_picker_category_nature">"Animais &amp; Natureza"</string>
<string name="emoji_picker_category_objects">"Objetos"</string>
<string name="emoji_picker_category_people">"Sorrisos &amp; Pessoas"</string>
<string name="emoji_picker_category_places">"Viagens &amp; Lugares"</string>
<string name="emoji_picker_category_symbols">"Símbolos"</string>
<string name="screen_report_content_block_user">"Bloquear usuário"</string>
<string name="screen_report_content_block_user_hint">"Marque se você deseja ocultar todas as mensagens atuais e futuras desse usuário"</string>
<string name="screen_report_content_explanation">"Essa mensagem será reportada ao administrador do seu homeserver. Eles não conseguirão ler nenhuma mensagem criptografada."</string>
<string name="screen_report_content_hint">"Motivo para denunciar este conteúdo"</string>
<string name="screen_room_attachment_source_camera">"Câmera"</string>
<string name="screen_room_attachment_source_camera_photo">"Tirar foto"</string>
<string name="screen_room_attachment_source_camera_video">"Gravar vídeo"</string>
<string name="screen_room_attachment_source_files">"Anexo"</string>
<string name="screen_room_attachment_source_gallery">"Biblioteca de fotos e vídeos"</string>
<string name="screen_room_attachment_source_location">"Localização"</string>
<string name="screen_room_attachment_source_poll">"Enquete"</string>
<string name="screen_room_attachment_text_formatting">"Formatação de texto"</string>
<string name="screen_room_encrypted_history_banner">"O histórico de mensagens não está disponível no momento."</string>
<string name="screen_room_invite_again_alert_message">"Gostaria de convidá-los de volta?"</string>
<string name="screen_room_invite_again_alert_title">"Você está sozinho neste chat"</string>
<string name="screen_room_mentions_at_room_title">"Todos"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Enviar novamente"</string>
<string name="screen_room_retry_send_menu_title">"Sua mensagem não foi enviada"</string>
<string name="screen_room_timeline_add_reaction">"Adicionar emoji"</string>
<string name="screen_room_timeline_beginning_of_room">"Este é o início do %1$s."</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"Este é o início desta conversa."</string>
<string name="screen_room_timeline_less_reactions">"Mostrar menos"</string>
<string name="screen_room_timeline_message_copied">"Mensagem copiada"</string>
<string name="screen_room_timeline_no_permission_to_post">"Você não tem permissão para postar nesta sala"</string>
<string name="screen_room_timeline_reactions_show_less">"Mostrar menos"</string>
<string name="screen_room_timeline_reactions_show_more">"Mostrar mais"</string>
<string name="screen_room_timeline_read_marker_title">"Novo"</string>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d mudança de sala"</item>
<item quantity="other">"%1$d mudanças de salas"</item>
</plurals>
</resources>

View file

@ -67,6 +67,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.MatrixRoom
@ -105,6 +106,7 @@ import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers
@ -138,7 +140,7 @@ class MessagesPresenterTest {
assertThat(initialState.roomAvatar)
.isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)))
assertThat(initialState.userHasPermissionToSendMessage).isTrue()
assertThat(initialState.userHasPermissionToRedactOwn).isFalse()
assertThat(initialState.userHasPermissionToRedactOwn).isTrue()
assertThat(initialState.hasNetworkConnection).isTrue()
assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
@ -149,7 +151,13 @@ class MessagesPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - check that the room's unread flag is removed`() = runTest {
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
assertThat(room.markAsReadCalls).isEmpty()
val presenter = createMessagesPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
@ -163,8 +171,13 @@ class MessagesPresenterTest {
@Test
fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest {
val room = FakeMatrixRoom().apply {
givenCanUserJoinCall(Result.success(false))
val room = FakeMatrixRoom(
canUserJoinCallResult = { Result.success(false) },
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
).apply {
givenRoomInfo(aRoomInfo(hasRoomCall = true))
}
val presenter = createMessagesPresenter(matrixRoom = room)
@ -185,7 +198,14 @@ class MessagesPresenterTest {
val timeline = FakeTimeline().apply {
this.toggleReactionLambda = toggleReactionSuccess
}
val room = FakeMatrixRoom(liveTimeline = timeline)
val room = FakeMatrixRoom(
liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -215,7 +235,14 @@ class MessagesPresenterTest {
val timeline = FakeTimeline().apply {
this.toggleReactionLambda = toggleReactionSuccess
}
val room = FakeMatrixRoom(liveTimeline = timeline)
val room = FakeMatrixRoom(
liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -268,6 +295,11 @@ class MessagesPresenterTest {
val event = aMessageEvent()
val matrixRoom = FakeMatrixRoom(
eventPermalinkResult = { Result.success("a link") },
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(
clipboardHelper = clipboardHelper,
@ -450,7 +482,14 @@ class MessagesPresenterTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
val liveTimeline = FakeTimeline()
val matrixRoom = FakeMatrixRoom(liveTimeline = liveTimeline)
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline,
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
liveTimeline.redactEventLambda = redactEventLambda
@ -515,7 +554,16 @@ class MessagesPresenterTest {
@Test
fun `present - shows prompt to reinvite users in DM`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID, isDirect = true, activeMemberCount = 1L)
val room = FakeMatrixRoom(
sessionId = A_SESSION_ID,
isDirect = true,
activeMemberCount = 1L,
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -541,7 +589,16 @@ class MessagesPresenterTest {
@Test
fun `present - doesn't show reinvite prompt in non-direct room`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID, isDirect = false, activeMemberCount = 1L)
val room = FakeMatrixRoom(
sessionId = A_SESSION_ID,
isDirect = false,
activeMemberCount = 1L,
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -556,7 +613,16 @@ class MessagesPresenterTest {
@Test
fun `present - doesn't show reinvite prompt if other party is present`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID, isDirect = true, activeMemberCount = 2L)
val room = FakeMatrixRoom(
sessionId = A_SESSION_ID,
isDirect = true,
activeMemberCount = 2L,
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -571,7 +637,16 @@ class MessagesPresenterTest {
@Test
fun `present - handle reinviting other user when memberlist is ready`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
val inviteUserResult = lambdaRecorder { _: UserId -> Result.success(Unit) }
val room = FakeMatrixRoom(
sessionId = A_SESSION_ID,
inviteUserResult = inviteUserResult,
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
room.givenRoomMembersState(
MatrixRoomMembersState.Ready(
persistentListOf(
@ -591,13 +666,22 @@ class MessagesPresenterTest {
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
val newState = awaitItem()
assertThat(newState.inviteProgress.isSuccess()).isTrue()
assertThat(room.invitedUserId).isEqualTo(A_SESSION_ID_2)
inviteUserResult.assertions().isCalledOnce().with(value(A_SESSION_ID_2))
}
}
@Test
fun `present - handle reinviting other user when memberlist is error`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
val inviteUserResult = lambdaRecorder { _: UserId -> Result.success(Unit) }
val room = FakeMatrixRoom(
sessionId = A_SESSION_ID,
inviteUserResult = inviteUserResult,
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
room.givenRoomMembersState(
MatrixRoomMembersState.Error(
failure = Throwable(),
@ -620,13 +704,20 @@ class MessagesPresenterTest {
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
val newState = awaitItem()
assertThat(newState.inviteProgress.isSuccess()).isTrue()
assertThat(room.invitedUserId).isEqualTo(A_SESSION_ID_2)
inviteUserResult.assertions().isCalledOnce().with(value(A_SESSION_ID_2))
}
}
@Test
fun `present - handle reinviting other user when memberlist is not ready`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
val room = FakeMatrixRoom(
sessionId = A_SESSION_ID,
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
val presenter = createMessagesPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
@ -644,7 +735,15 @@ class MessagesPresenterTest {
@Test
fun `present - handle reinviting other user when inviting fails`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
val room = FakeMatrixRoom(
sessionId = A_SESSION_ID,
inviteUserResult = { Result.failure(Throwable("Oops!")) },
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
room.givenRoomMembersState(
MatrixRoomMembersState.Ready(
persistentListOf(
@ -653,7 +752,6 @@ class MessagesPresenterTest {
)
)
)
room.givenInviteUserResult(Result.failure(Throwable("Oops!")))
val presenter = createMessagesPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -673,8 +771,19 @@ class MessagesPresenterTest {
@Test
fun `present - permission to post`() = runTest {
val matrixRoom = FakeMatrixRoom()
matrixRoom.givenCanSendEventResult(MessageEventType.ROOM_MESSAGE, Result.success(true))
val matrixRoom = FakeMatrixRoom(
canUserSendMessageResult = { _, messageEventType ->
when (messageEventType) {
MessageEventType.ROOM_MESSAGE -> Result.success(true)
MessageEventType.REACTION -> Result.success(true)
else -> lambdaError()
}
},
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -686,8 +795,19 @@ class MessagesPresenterTest {
@Test
fun `present - no permission to post`() = runTest {
val matrixRoom = FakeMatrixRoom()
matrixRoom.givenCanSendEventResult(MessageEventType.ROOM_MESSAGE, Result.success(false))
val matrixRoom = FakeMatrixRoom(
canUserSendMessageResult = { _, messageEventType ->
when (messageEventType) {
MessageEventType.ROOM_MESSAGE -> Result.success(false)
MessageEventType.REACTION -> Result.success(false)
else -> lambdaError()
}
},
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -702,7 +822,13 @@ class MessagesPresenterTest {
@Test
fun `present - permission to redact own`() = runTest {
val matrixRoom = FakeMatrixRoom(canRedactOwn = true)
val matrixRoom = FakeMatrixRoom(
canRedactOwnResult = { Result.success(true) },
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOtherResult = { Result.success(false) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -716,7 +842,13 @@ class MessagesPresenterTest {
@Test
fun `present - permission to redact other`() = runTest {
val matrixRoom = FakeMatrixRoom(canRedactOther = true)
val matrixRoom = FakeMatrixRoom(
canRedactOtherResult = { Result.success(true) },
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(false) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -756,7 +888,13 @@ class MessagesPresenterTest {
private fun TestScope.createMessagesPresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
matrixRoom: MatrixRoom = FakeMatrixRoom(
canUserSendMessageResult = { _, _ -> Result.success(true) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) },
).apply {
givenRoomInfo(aRoomInfo(id = roomId, name = ""))
},
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),

View file

@ -26,7 +26,9 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
import io.element.android.features.messages.impl.attachments.preview.SendActionState
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
@ -34,6 +36,7 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
@ -49,13 +52,16 @@ class AttachmentsPreviewPresenterTest {
@Test
fun `present - send media success scenario`() = runTest {
val room = FakeMatrixRoom()
room.givenProgressCallbackValues(
listOf(
val sendMediaResult = lambdaRecorder<ProgressCallback?, Result<FakeMediaUploadHandler>> {
Result.success(FakeMediaUploadHandler())
}
val room = FakeMatrixRoom(
progressCallbackValues = listOf(
Pair(0, 10),
Pair(5, 10),
Pair(10, 10)
)
),
sendMediaResult = sendMediaResult,
)
val presenter = createAttachmentsPreviewPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
@ -70,15 +76,19 @@ class AttachmentsPreviewPresenterTest {
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f))
val successState = awaitItem()
assertThat(successState.sendActionState).isEqualTo(SendActionState.Done)
assertThat(room.sendMediaCount).isEqualTo(1)
sendMediaResult.assertions().isCalledOnce()
}
}
@Test
fun `present - send media failure scenario`() = runTest {
val room = FakeMatrixRoom()
val failure = MediaPreProcessor.Failure(null)
room.givenSendMediaResult(Result.failure(failure))
val sendMediaResult = lambdaRecorder<ProgressCallback?, Result<FakeMediaUploadHandler>> {
Result.failure(failure)
}
val room = FakeMatrixRoom(
sendMediaResult = sendMediaResult,
)
val presenter = createAttachmentsPreviewPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -90,7 +100,7 @@ class AttachmentsPreviewPresenterTest {
assertThat(loadingState.sendActionState).isEqualTo(SendActionState.Sending.Processing)
val failureState = awaitItem()
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure))
assertThat(room.sendMediaCount).isEqualTo(0)
sendMediaResult.assertions().isCalledOnce()
failureState.eventSink(AttachmentsPreviewEvents.ClearSendState)
val clearedState = awaitItem()
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Idle)

View file

@ -22,11 +22,14 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -81,7 +84,12 @@ class ReportMessagePresenterTest {
@Test
fun `presenter - handle successful report and block user`() = runTest {
val room = FakeMatrixRoom()
val reportContentResult = lambdaRecorder<EventId, String, UserId?, Result<Unit>> { _, _, _ ->
Result.success(Unit)
}
val room = FakeMatrixRoom(
reportContentResult = reportContentResult
)
val presenter = createReportMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -92,13 +100,18 @@ class ReportMessagePresenterTest {
initialState.eventSink(ReportMessageEvents.Report)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Success::class.java)
assertThat(room.reportedContentCount).isEqualTo(1)
reportContentResult.assertions().isCalledOnce()
}
}
@Test
fun `presenter - handle successful report`() = runTest {
val room = FakeMatrixRoom()
val reportContentResult = lambdaRecorder<EventId, String, UserId?, Result<Unit>> { _, _, _ ->
Result.success(Unit)
}
val room = FakeMatrixRoom(
reportContentResult = reportContentResult
)
val presenter = createReportMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -107,15 +120,18 @@ class ReportMessagePresenterTest {
initialState.eventSink(ReportMessageEvents.Report)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Success::class.java)
assertThat(room.reportedContentCount).isEqualTo(1)
reportContentResult.assertions().isCalledOnce()
}
}
@Test
fun `presenter - handle failed report`() = runTest {
val room = FakeMatrixRoom().apply {
givenReportContentResult(Result.failure(Exception("Failed to report content")))
val reportContentResult = lambdaRecorder<EventId, String, UserId?, Result<Unit>> { _, _, _ ->
Result.failure(Exception("Failed to report content"))
}
val room = FakeMatrixRoom(
reportContentResult = reportContentResult
)
val presenter = createReportMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -125,7 +141,7 @@ class ReportMessagePresenterTest {
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Loading::class.java)
val resultState = awaitItem()
assertThat(resultState.result).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(room.reportedContentCount).isEqualTo(1)
reportContentResult.assertions().isCalledOnce()
resultState.eventSink(ReportMessageEvents.ClearError)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Uninitialized::class.java)

View file

@ -43,6 +43,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.ImageInfo
@ -55,6 +56,7 @@ import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
import io.element.android.libraries.matrix.test.AN_EVENT_ID
@ -67,6 +69,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.test.A_USER_ID_4
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
@ -297,7 +300,13 @@ class MessageComposerPresenterTest {
@Test
fun `present - send message with rich text enabled`() = runTest {
val presenter = createPresenter(this)
val presenter = createPresenter(
coroutineScope = this,
room = FakeMatrixRoom(
sendMessageResult = { _, _, _ -> Result.success(Unit) },
typingNoticeResult = { Result.success(Unit) }
),
)
moleculeFlow(RecompositionMode.Immediate) {
val state = presenter.present()
remember(state, state.textEditorState.messageHtml()) { state }
@ -324,7 +333,14 @@ class MessageComposerPresenterTest {
@Test
fun `present - send message with plain text enabled`() = runTest {
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("") })
val presenter = createPresenter(this, isRichTextEditorEnabled = false)
val presenter = createPresenter(
coroutineScope = this,
isRichTextEditorEnabled = false,
room = FakeMatrixRoom(
sendMessageResult = { _, _, _ -> Result.success(Unit) },
typingNoticeResult = { Result.success(Unit) }
),
)
moleculeFlow(RecompositionMode.Immediate) {
val state = presenter.present()
val messageMarkdown = state.textEditorState.messageMarkdown(permalinkBuilder)
@ -358,7 +374,10 @@ class MessageComposerPresenterTest {
val timeline = FakeTimeline().apply {
this.editMessageLambda = editMessageLambda
}
val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline)
val fakeMatrixRoom = FakeMatrixRoom(
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(
this,
fakeMatrixRoom,
@ -399,6 +418,67 @@ class MessageComposerPresenterTest {
}
}
@Test
fun `present - edit sent message event not found`() = runTest {
val timelineEditMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
Result.failure<Unit>(TimelineException.EventNotFound)
}
val timeline = FakeTimeline().apply {
this.editMessageLambda = timelineEditMessageLambda
}
val roomEditMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List<Mention> ->
Result.success(Unit)
}
val fakeMatrixRoom = FakeMatrixRoom(
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) }
).apply {
this.editMessageLambda = roomEditMessageLambda
}
val presenter = createPresenter(
this,
fakeMatrixRoom,
)
moleculeFlow(RecompositionMode.Immediate) {
val state = presenter.present()
remember(state, state.textEditorState.messageHtml()) { state }
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.textEditorState.messageHtml()).isEqualTo("")
val mode = anEditMode()
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
val withMessageState = awaitItem()
assertThat(withMessageState.mode).isEqualTo(mode)
assertThat(withMessageState.textEditorState.messageHtml()).isEqualTo(A_MESSAGE)
withMessageState.textEditorState.setHtml(ANOTHER_MESSAGE)
val withEditedMessageState = awaitItem()
assertThat(withEditedMessageState.textEditorState.messageHtml()).isEqualTo(ANOTHER_MESSAGE)
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage)
skipItems(1)
val messageSentState = awaitItem()
assertThat(messageSentState.textEditorState.messageHtml()).isEqualTo("")
advanceUntilIdle()
assert(timelineEditMessageLambda)
.isCalledOnce()
.with(value(AN_EVENT_ID), value(null), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any())
assert(roomEditMessageLambda)
.isCalledOnce()
.with(value(AN_EVENT_ID), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any())
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
inThread = false,
isEditing = true,
isReply = false,
messageType = Composer.MessageType.Text,
)
)
}
}
@Test
fun `present - edit not sent message`() = runTest {
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
@ -407,7 +487,10 @@ class MessageComposerPresenterTest {
val timeline = FakeTimeline().apply {
this.editMessageLambda = editMessageLambda
}
val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline)
val fakeMatrixRoom = FakeMatrixRoom(
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) },
)
val presenter = createPresenter(
this,
fakeMatrixRoom,
@ -456,7 +539,10 @@ class MessageComposerPresenterTest {
val timeline = FakeTimeline().apply {
this.replyMessageLambda = replyMessageLambda
}
val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline)
val fakeMatrixRoom = FakeMatrixRoom(
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(
this,
fakeMatrixRoom,
@ -524,7 +610,9 @@ class MessageComposerPresenterTest {
@Test
fun `present - Pick image from gallery`() = runTest {
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
pickerProvider.givenMimeType(MimeTypes.Images)
mediaPreProcessor.givenResult(
@ -557,7 +645,9 @@ class MessageComposerPresenterTest {
@Test
fun `present - Pick video from gallery`() = runTest {
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
pickerProvider.givenMimeType(MimeTypes.Videos)
mediaPreProcessor.givenResult(
@ -607,13 +697,17 @@ class MessageComposerPresenterTest {
@Test
fun `present - Pick file from storage`() = runTest {
val room = FakeMatrixRoom()
room.givenProgressCallbackValues(
listOf(
val sendMediaResult = lambdaRecorder { _: ProgressCallback? ->
Result.success(FakeMediaUploadHandler())
}
val room = FakeMatrixRoom(
progressCallbackValues = listOf(
Pair(0, 10),
Pair(5, 10),
Pair(10, 10)
)
),
sendMediaResult = sendMediaResult,
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
moleculeFlow(RecompositionMode.Immediate) {
@ -629,13 +723,15 @@ class MessageComposerPresenterTest {
assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(1f))
val sentState = awaitItem()
assertThat(sentState.attachmentsState).isEqualTo(AttachmentsState.None)
assertThat(room.sendMediaCount).isEqualTo(1)
sendMediaResult.assertions().isCalledOnce()
}
}
@Test
fun `present - create poll`() = runTest {
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -652,7 +748,9 @@ class MessageComposerPresenterTest {
@Test
fun `present - share location`() = runTest {
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -669,7 +767,9 @@ class MessageComposerPresenterTest {
@Test
fun `present - Take photo`() = runTest {
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
val presenter = createPresenter(
this,
@ -689,7 +789,9 @@ class MessageComposerPresenterTest {
@Test
fun `present - Take photo with permission request`() = runTest {
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter()
val presenter = createPresenter(
this,
@ -714,7 +816,9 @@ class MessageComposerPresenterTest {
@Test
fun `present - Record video`() = runTest {
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
val presenter = createPresenter(
this,
@ -734,7 +838,9 @@ class MessageComposerPresenterTest {
@Test
fun `present - Record video with permission request`() = runTest {
val room = FakeMatrixRoom()
val room = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
)
val permissionPresenter = FakePermissionsPresenter()
val presenter = createPresenter(
this,
@ -759,9 +865,10 @@ class MessageComposerPresenterTest {
@Test
fun `present - Uploading media failure can be recovered from`() = runTest {
val room = FakeMatrixRoom().apply {
givenSendMediaResult(Result.failure(Exception()))
}
val room = FakeMatrixRoom(
sendMediaResult = { Result.failure(Exception()) },
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -842,15 +949,17 @@ class MessageComposerPresenterTest {
val invitedUser = aRoomMember(userId = A_USER_ID_3, membership = RoomMembershipState.INVITE)
val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN)
val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN)
var canUserTriggerRoomNotificationResult = true
val room = FakeMatrixRoom(
isDirect = false,
canUserTriggerRoomNotificationResult = { Result.success(canUserTriggerRoomNotificationResult) },
typingNoticeResult = { Result.success(Unit) }
).apply {
givenRoomMembersState(
MatrixRoomMembersState.Ready(
persistentListOf(currentUser, invitedUser, bob, david),
)
)
givenCanTriggerRoomNotification(Result.success(true))
}
val flagsService = FakeFeatureFlagService(
mapOf(
@ -890,13 +999,10 @@ class MessageComposerPresenterTest {
assertThat(awaitItem().memberSuggestions).isEmpty()
// If user has no permission to send `@room` mentions, `RoomMemberSuggestion.Room` is not returned
room.givenCanTriggerRoomNotification(Result.success(false))
canUserTriggerRoomNotificationResult = false
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
assertThat(awaitItem().memberSuggestions)
.containsExactly(ResolvedMentionSuggestion.Member(bob), ResolvedMentionSuggestion.Member(david))
// If room is a DM, `RoomMemberSuggestion.Room` is not returned
room.givenCanTriggerRoomNotification(Result.success(true))
}
}
@ -910,13 +1016,14 @@ class MessageComposerPresenterTest {
isDirect = true,
activeMemberCount = 2,
isEncrypted = true,
canUserTriggerRoomNotificationResult = { Result.success(true) },
typingNoticeResult = { Result.success(Unit) }
).apply {
givenRoomMembersState(
MatrixRoomMembersState.Ready(
persistentListOf(currentUser, invitedUser, bob, david),
)
)
givenCanTriggerRoomNotification(Result.success(true))
}
val flagsService = FakeFeatureFlagService(
mapOf(
@ -973,7 +1080,14 @@ class MessageComposerPresenterTest {
this.replyMessageLambda = replyMessageLambda
this.editMessageLambda = editMessageLambda
}
val room = FakeMatrixRoom(liveTimeline = timeline)
val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List<Mention> ->
Result.success(Unit)
}
val room = FakeMatrixRoom(
liveTimeline = timeline,
sendMessageResult = sendMessageResult,
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(room = room, coroutineScope = this)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -993,7 +1107,8 @@ class MessageComposerPresenterTest {
advanceUntilIdle()
assertThat(room.sendMessageMentions).isEqualTo(listOf(Mention.User(A_USER_ID)))
sendMessageResult.assertions().isCalledOnce()
.with(value(A_MESSAGE), any(), value(listOf(Mention.User(A_USER_ID))))
// Check intentional mentions on reply sent
initialState.eventSink(MessageComposerEvents.SetMode(aReplyMode()))
@ -1049,22 +1164,32 @@ class MessageComposerPresenterTest {
@Test
fun `present - handle typing notice event`() = runTest {
val room = FakeMatrixRoom()
val typingNoticeResult = lambdaRecorder<Boolean, Result<Unit>> { Result.success(Unit) }
val room = FakeMatrixRoom(
typingNoticeResult = typingNoticeResult
)
val presenter = createPresenter(room = room, coroutineScope = this)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(room.typingRecord).isEmpty()
typingNoticeResult.assertions().isNeverCalled()
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(true))
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(false))
assertThat(room.typingRecord).isEqualTo(listOf(true, false))
typingNoticeResult.assertions().isCalledExactly(2)
.withSequence(
listOf(value(true)),
listOf(value(false)),
)
}
}
@Test
fun `present - handle typing notice event when sending typing notice is disabled`() = runTest {
val room = FakeMatrixRoom()
val typingNoticeResult = lambdaRecorder<Boolean, Result<Unit>> { Result.success(Unit) }
val room = FakeMatrixRoom(
typingNoticeResult = typingNoticeResult
)
val store = InMemorySessionPreferencesStore(
isSendTypingNotificationsEnabled = false
)
@ -1073,10 +1198,10 @@ class MessageComposerPresenterTest {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(room.typingRecord).isEmpty()
typingNoticeResult.assertions().isNeverCalled()
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(true))
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(false))
assertThat(room.typingRecord).isEmpty()
typingNoticeResult.assertions().isNeverCalled()
}
}
@ -1215,7 +1340,10 @@ class MessageComposerPresenterTest {
val timeline = FakeTimeline().apply {
this.loadReplyDetailsLambda = loadReplyDetailsLambda
}
val room = FakeMatrixRoom(liveTimeline = timeline)
val room = FakeMatrixRoom(
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) },
)
val permalinkBuilder = FakePermalinkBuilder()
val presenter = createPresenter(
room = room,
@ -1352,7 +1480,9 @@ class MessageComposerPresenterTest {
private fun createPresenter(
coroutineScope: CoroutineScope,
room: MatrixRoom = FakeMatrixRoom(),
room: MatrixRoom = FakeMatrixRoom(
typingNoticeResult = { Result.success(Unit) }
),
pickerProvider: PickerProvider = this.pickerProvider,
featureFlagService: FeatureFlagService = this.featureFlagService,
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.test.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
@ -38,9 +39,9 @@ class TimelineControllerTest {
val liveTimeline = FakeTimeline(name = "live")
val detachedTimeline = FakeTimeline(name = "detached")
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = { Result.success(detachedTimeline) }
)
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline))
val sut = TimelineController(matrixRoom)
sut.activeTimelineFlow().test {
@ -68,8 +69,17 @@ class TimelineControllerTest {
val liveTimeline = FakeTimeline(name = "live")
val detachedTimeline1 = FakeTimeline(name = "detached 1")
val detachedTimeline2 = FakeTimeline(name = "detached 2")
var callNumber = 0
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = {
callNumber++
when (callNumber) {
1 -> Result.success(detachedTimeline1)
2 -> Result.success(detachedTimeline2)
else -> lambdaError()
}
}
)
val sut = TimelineController(matrixRoom)
@ -77,7 +87,6 @@ class TimelineControllerTest {
awaitItem().also { state ->
assertThat(state).isEqualTo(liveTimeline)
}
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline1))
sut.focusOnEvent(AN_EVENT_ID)
awaitItem().also { state ->
assertThat(state).isEqualTo(detachedTimeline1)
@ -85,7 +94,6 @@ class TimelineControllerTest {
assertThat(detachedTimeline1.closeCounter).isEqualTo(0)
assertThat(detachedTimeline2.closeCounter).isEqualTo(0)
// Focus on another event should close the previous detached timeline
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline2))
sut.focusOnEvent(AN_EVENT_ID)
awaitItem().also { state ->
assertThat(state).isEqualTo(detachedTimeline2)
@ -117,11 +125,10 @@ class TimelineControllerTest {
val liveTimeline = FakeTimeline(name = "live")
val detachedTimeline = FakeTimeline(name = "detached")
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = { Result.success(detachedTimeline) }
)
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline))
val sut = TimelineController(matrixRoom)
sut.activeTimelineFlow().test {
awaitItem().also { state ->
assertThat(state).isEqualTo(liveTimeline)
@ -168,9 +175,9 @@ class TimelineControllerTest {
sendMessageLambda = lambdaForDetached
}
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = { Result.success(detachedTimeline) }
)
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline))
val sut = TimelineController(matrixRoom)
sut.activeTimelineFlow().test {
sut.focusOnEvent(AN_EVENT_ID)
@ -193,9 +200,9 @@ class TimelineControllerTest {
val liveTimeline = FakeTimeline(name = "live")
val detachedTimeline = FakeTimeline(name = "detached")
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = { Result.success(detachedTimeline) }
)
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline))
val sut = TimelineController(matrixRoom)
sut.activeTimelineFlow().test {

View file

@ -133,7 +133,10 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
)
)
)
val room = FakeMatrixRoom(liveTimeline = timeline)
val room = FakeMatrixRoom(
liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) },
)
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
val presenter = createTimelinePresenter(
timeline = timeline,
@ -482,9 +485,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
)
val room = FakeMatrixRoom(
liveTimeline = liveTimeline,
).apply {
givenTimelineFocusedOnEventResult(Result.success(detachedTimeline))
}
timelineFocusedOnEventResult = { Result.success(detachedTimeline) },
canUserSendMessageResult = { _, _ -> Result.success(true) },
)
val presenter = createTimelinePresenter(
room = room,
)
@ -529,6 +532,7 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
)
)
),
canUserSendMessageResult = { _, _ -> Result.success(true) },
),
timelineItemIndexer = timelineItemIndexer,
)
@ -551,9 +555,9 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
liveTimeline = FakeTimeline(
timelineItems = flowOf(emptyList()),
),
).apply {
givenTimelineFocusedOnEventResult(Result.failure(Throwable("An error")))
},
timelineFocusedOnEventResult = { Result.failure(Throwable("An error")) },
canUserSendMessageResult = { _, _ -> Result.success(true) },
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -594,7 +598,10 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
)
)
)
val room = FakeMatrixRoom(liveTimeline = timeline).apply {
val room = FakeMatrixRoom(
liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) },
).apply {
givenRoomMembersState(MatrixRoomMembersState.Unknown)
}
@ -626,7 +633,10 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
private fun TestScope.createTimelinePresenter(
timeline: Timeline = FakeTimeline(),
room: FakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline),
room: FakeMatrixRoom = FakeMatrixRoom(
liveTimeline = timeline,
canUserSendMessageResult = { _, _ -> Result.success(true) }
),
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),

View file

@ -20,9 +20,11 @@ import android.net.Uri
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.SpannedString
import android.text.style.URLSpan
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.text.toSpannable
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
@ -74,6 +76,7 @@ import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorW
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.test.runTest
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@ -195,7 +198,7 @@ class TimelineItemContentMessageFactoryTest {
inSpans(URLSpan("https://matrix.org")) {
append("and manually added link")
}
}
}.toSpannable()
val sut = createTimelineItemContentMessageFactory(
htmlConverterTransform = { expected }
)
@ -610,7 +613,7 @@ class TimelineItemContentMessageFactoryTest {
senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
assertThat((result as TimelineItemNoticeContent).formattedBody).isEqualTo("formatted")
(result as TimelineItemNoticeContent).formattedBody.assertSpannedEquals(SpannedString("formatted"))
}
@Test
@ -644,7 +647,8 @@ class TimelineItemContentMessageFactoryTest {
senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
assertThat((result as TimelineItemEmoteContent).formattedBody).isEqualTo(SpannableString("* Bob formatted"))
(result as TimelineItemEmoteContent).formattedBody.assertSpannedEquals(SpannableString("* Bob formatted"))
}
@Test
@ -654,7 +658,7 @@ class TimelineItemContentMessageFactoryTest {
inSpans(URLSpan("https://www.example.org")) {
append("me@matrix.org")
}
}
}.toSpannable()
val sut = createTimelineItemContentMessageFactory(
htmlConverterTransform = { expectedSpanned },
permalinkParser = FakePermalinkParser { PermalinkData.FallbackLink(Uri.EMPTY) }
@ -669,7 +673,59 @@ class TimelineItemContentMessageFactoryTest {
senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expectedSpanned)
(result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned)
}
@Test
fun `a message with plain URL in a formatted body Spanned format gets linkified too`() = runTest {
val expectedSpanned = buildSpannedString {
append("Test ")
inSpansWithFlags(URLSpan("https://www.example.org"), flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) {
append("https://www.example.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 https://www.example.org")
)
),
senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
(result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned)
}
@Test
fun `a message with plain URL in a formatted body with plain text format gets linkified too`() = runTest {
val resultString = "Test https://www.example.org"
val expectedSpanned = buildSpannedString {
append("Test ")
inSpansWithFlags(URLSpan("https://www.example.org"), flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) {
append("https://www.example.org")
}
}.toSpannable()
val sut = createTimelineItemContentMessageFactory(
htmlConverterTransform = { resultString },
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 https://www.example.org")
)
),
senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
(result as TimelineItemTextContent).formattedBody.assertSpannedEquals(expectedSpanned)
}
private fun createMessageContent(
@ -718,3 +774,40 @@ class TimelineItemContentMessageFactoryTest {
fileExtensionExtractor = FileExtensionExtractorWithoutValidation()
)
}
private inline fun SpannableStringBuilder.inSpansWithFlags(span: Any, flags: Int, action: SpannableStringBuilder.() -> Unit) {
val start = this.length
action()
val end = this.length
setSpan(span, start, end, flags)
}
fun CharSequence?.assertSpannedEquals(other: CharSequence?) {
if (this == null && other == null) {
return
} else if (this is Spanned && other is Spanned) {
assertThat(this.toString()).isEqualTo(other.toString())
assertThat(this.length).isEqualTo(other.length)
val thisSpans = this.getSpans(0, this.length, Any::class.java)
val otherSpans = other.getSpans(0, other.length, Any::class.java)
if (thisSpans.size != otherSpans.size) {
fail("Expected ${thisSpans.size} spans, got ${otherSpans.size}")
}
thisSpans.forEachIndexed { index, span ->
val otherSpan = otherSpans[index]
// URLSpans don't have a proper `equals` implementation, so we compare the URL instead
if (span is URLSpan && otherSpan is URLSpan) {
assertThat(span.url).isEqualTo(otherSpan.url)
} else {
assertThat(span).isEqualTo(otherSpan)
}
assertThat(this.getSpanStart(span)).isEqualTo(other.getSpanStart(otherSpan))
assertThat(this.getSpanEnd(span)).isEqualTo(other.getSpanEnd(otherSpan))
assertThat(this.getSpanFlags(span)).isEqualTo(other.getSpanFlags(otherSpan))
}
} else {
val thisString = this?.toString() ?: "null"
val otherString = other?.toString() ?: "null"
fail("Expected Spanned, got $thisString and $otherString")
}
}

View file

@ -28,7 +28,9 @@ import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
@ -45,6 +47,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -63,7 +66,10 @@ class VoiceMessageComposerPresenterTest {
recordingDuration = RECORDING_DURATION
)
private val analyticsService = FakeAnalyticsService()
private val matrixRoom = FakeMatrixRoom()
private val sendMediaResult = lambdaRecorder<ProgressCallback?, Result<FakeMediaUploadHandler>> { Result.success(FakeMediaUploadHandler()) }
private val matrixRoom = FakeMatrixRoom(
sendMediaResult = sendMediaResult
)
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom)
private val messageComposerContext = FakeMessageComposerContext()
@ -295,7 +301,7 @@ class VoiceMessageComposerPresenterTest {
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
assertThat(matrixRoom.sendMediaCount).isEqualTo(1)
sendMediaResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
testPauseAndDestroy(finalState)
@ -346,7 +352,7 @@ class VoiceMessageComposerPresenterTest {
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
assertThat(matrixRoom.sendMediaCount).isEqualTo(1)
sendMediaResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
testPauseAndDestroy(finalState)
@ -369,7 +375,7 @@ class VoiceMessageComposerPresenterTest {
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
assertThat(matrixRoom.sendMediaCount).isEqualTo(1)
sendMediaResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
testPauseAndDestroy(finalState)
@ -393,7 +399,7 @@ class VoiceMessageComposerPresenterTest {
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true))
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
sendMediaResult.assertions().isNeverCalled()
assertThat(analyticsService.trackedErrors).hasSize(0)
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
@ -418,13 +424,13 @@ class VoiceMessageComposerPresenterTest {
ensureAllEventsConsumed()
assertThat(previewState.voiceMessageState).isEqualTo(aPreviewState())
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
sendMediaResult.assertions().isNeverCalled()
mediaPreProcessor.givenAudioResult()
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
assertThat(matrixRoom.sendMediaCount).isEqualTo(1)
sendMediaResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
testPauseAndDestroy(finalState)
@ -461,7 +467,7 @@ class VoiceMessageComposerPresenterTest {
assertThat(showSendFailureDialog).isFalse()
}
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
sendMediaResult.assertions().isNeverCalled()
testPauseAndDestroy(finalState)
}
}
@ -477,7 +483,7 @@ class VoiceMessageComposerPresenterTest {
initialState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
sendMediaResult.assertions().isNeverCalled()
assertThat(analyticsService.trackedErrors).hasSize(1)
voiceRecorder.assertCalls(started = 0)
@ -496,7 +502,7 @@ class VoiceMessageComposerPresenterTest {
val initialState = awaitItem()
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
sendMediaResult.assertions().isNeverCalled()
assertThat(analyticsService.trackedErrors).containsExactly(
VoiceMessageException.PermissionMissing(message = "Expected permission to record but none", cause = exception)
)

View file

@ -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">"Zaloguj się ręcznie"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Zaloguj się za pomocą kodu QR"</string>
<string name="screen_onboarding_sign_up">"Utwórz konto"</string>
<string name="screen_onboarding_welcome_message">"Witamy w %1$s. Szybszy i prostszy niż kiedykolwiek."</string>
<string name="screen_onboarding_welcome_subtitle">"Witamy w %1$s. Doładowany, dla szybkości i prostoty."</string>
<string name="screen_onboarding_welcome_title">"Be in your element"</string>
</resources>

View file

@ -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_onboarding_sign_in_manually">"Iniciar sessão manualmente"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Iniciar sessão com código QR"</string>
<string name="screen_onboarding_sign_up">"Criar conta"</string>
<string name="screen_onboarding_welcome_message">"Bem-vindo ao mais rápido %1$s de todos os tempos. Turbinado para velocidade e simplicidade."</string>
<string name="screen_onboarding_welcome_subtitle">"Bem-vindo ao %1$s. Turbinado, para velocidade e simplicidade"</string>
</resources>

View file

@ -4,8 +4,16 @@
<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_cancel_confirmation_content_android">"თქვენი ცვლილებები არ არის შენახული. დარწმუნებული ხართ, რომ გსურთ დაბრუნება?"</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_delete_confirmation">"დარწმუნებული ხართ, რომ გსურთ ამ გამოკითხვის წაშლა?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"გამოკითხვის წაშლა"</string>
<string name="screen_edit_poll_title">"გამოკითხვის რედაქტირება"</string>
<string name="screen_polls_history_empty_ongoing">"მიმდინარე გამოკითხვები ვერ მოიძებნა."</string>
<string name="screen_polls_history_empty_past">"ბოლო გამოკითხვების მოძებნა ვერ მოხერხდა."</string>
<string name="screen_polls_history_filter_ongoing">"მიმდინარე"</string>
<string name="screen_polls_history_filter_past">"წარსული"</string>
<string name="screen_polls_history_title">"გამოკითხვები"</string>
</resources>

View file

@ -0,0 +1,19 @@
<?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">"Dodaj opcję"</string>
<string name="screen_create_poll_anonymous_desc">"Pokaż wyniki dopiero po zakończeniu ankiety"</string>
<string name="screen_create_poll_anonymous_headline">"Ukryj głosy"</string>
<string name="screen_create_poll_answer_hint">"Opcja %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Twoje zmiany nie zostały zapisane. Czy na pewno chcesz wrócić?"</string>
<string name="screen_create_poll_question_desc">"Pytanie lub temat"</string>
<string name="screen_create_poll_question_hint">"Czego dotyczy ankieta?"</string>
<string name="screen_create_poll_title">"Utwórz ankietę"</string>
<string name="screen_edit_poll_delete_confirmation">"Czy na pewno chcesz usunąć tę ankietę?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Usuń ankietę"</string>
<string name="screen_edit_poll_title">"Edytuj ankietę"</string>
<string name="screen_polls_history_empty_ongoing">"Nie znaleziono ankiet w trakcie."</string>
<string name="screen_polls_history_empty_past">"Nie znaleziono ankiet."</string>
<string name="screen_polls_history_filter_ongoing">"W trakcie"</string>
<string name="screen_polls_history_filter_past">"Przeszłe"</string>
<string name="screen_polls_history_title">"Ankiety"</string>
</resources>

View file

@ -0,0 +1,12 @@
<?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">"Adicionar opção"</string>
<string name="screen_create_poll_anonymous_desc">"Mostrar resultados somente após o término da enquete"</string>
<string name="screen_create_poll_anonymous_headline">"Ocultar votos"</string>
<string name="screen_create_poll_answer_hint">"Opção %1$d"</string>
<string name="screen_create_poll_question_desc">"Pergunta ou tópico"</string>
<string name="screen_create_poll_question_hint">"Sobre o que é a enquete?"</string>
<string name="screen_create_poll_title">"Criar enquete"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Excluir Enquete"</string>
<string name="screen_edit_poll_title">"Editar enquete"</string>
</resources>

View file

@ -35,7 +35,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.SavePollInvocation
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider
import io.element.android.services.analytics.test.FakeAnalyticsService
@ -51,9 +50,9 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class) class CreatePollPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@OptIn(ExperimentalCoroutinesApi::class)
class CreatePollPresenterTest {
@get:Rule val warmUpRule = WarmUpRule()
private val pollEventId = AN_EVENT_ID
private var navUpInvocationsCount = 0
@ -128,7 +127,13 @@ import org.junit.Test
@Test
fun `create poll sends a poll start event`() = runTest {
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
val createPollResult = lambdaRecorder<String, List<String>, Int, PollKind, Result<Unit>> { _, _, _, _ -> Result.success(Unit) }
val presenter = createCreatePollPresenter(
room = FakeMatrixRoom(
createPollResult = createPollResult
),
mode = CreatePollMode.NewPoll,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -139,15 +144,13 @@ import org.junit.Test
skipItems(3)
initial.eventSink(CreatePollEvents.Save)
delay(1) // Wait for the coroutine to finish
assertThat(fakeMatrixRoom.createPollInvocations.size).isEqualTo(1)
assertThat(fakeMatrixRoom.createPollInvocations.last()).isEqualTo(
SavePollInvocation(
question = "A question?",
answers = listOf("Answer 1", "Answer 2"),
maxSelections = 1,
pollKind = PollKind.Disclosed
createPollResult.assertions().isCalledOnce()
.with(
value("A question?"),
value(listOf("Answer 1", "Answer 2")),
value(1),
value(PollKind.Disclosed),
)
)
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2)
assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo(
Composer(
@ -170,8 +173,15 @@ import org.junit.Test
@Test
fun `when poll creation fails, error is tracked`() = runTest {
val error = Exception("cause")
fakeMatrixRoom.givenCreatePollResult(Result.failure(error))
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
val createPollResult = lambdaRecorder<String, List<String>, Int, PollKind, Result<Unit>> { _, _, _, _ ->
Result.failure(error)
}
val presenter = createCreatePollPresenter(
room = FakeMatrixRoom(
createPollResult = createPollResult
),
mode = CreatePollMode.NewPoll,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -180,7 +190,7 @@ import org.junit.Test
awaitItem().eventSink(CreatePollEvents.SetAnswer(1, "Answer 2"))
awaitItem().eventSink(CreatePollEvents.Save)
delay(1) // Wait for the coroutine to finish
assertThat(fakeMatrixRoom.createPollInvocations).hasSize(1)
createPollResult.assertions().isCalledOnce()
assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
assertThat(fakeAnalyticsService.trackedErrors).hasSize(1)
assertThat(fakeAnalyticsService.trackedErrors).containsExactly(
@ -252,14 +262,22 @@ import org.junit.Test
@Test
fun `when edit poll fails, error is tracked`() = runTest {
val error = Exception("cause")
val editPollResult = lambdaRecorder { _: EventId, _: String, _: List<String>, _: Int, _: PollKind ->
Result.failure<Unit>(error)
}
val presenter = createCreatePollPresenter(
room = FakeMatrixRoom(
editPollResult = editPollResult,
liveTimeline = timeline,
),
mode = CreatePollMode.EditPoll(pollEventId),
)
val editPollLambda = lambdaRecorder { _: EventId, _: String, _: List<String>, _: Int, _: PollKind ->
Result.failure<Unit>(error)
}
timeline.apply {
this.editPollLambda = editPollLambda
}
fakeMatrixRoom.givenEditPollResult(Result.failure(error))
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {

View file

@ -90,6 +90,7 @@ dependencies {
testImplementation(projects.features.ftue.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl)
testImplementation(projects.features.logout.test)
testImplementation(projects.features.roomlist.test)
testImplementation(projects.libraries.indicator.impl)
testImplementation(projects.libraries.pushproviders.test)

View file

@ -21,5 +21,6 @@ import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
sealed interface DeveloperSettingsEvents {
data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents
data class SetSimplifiedSlidingSyncEnabled(val isEnabled: Boolean) : DeveloperSettingsEvents
data object ClearCache : DeveloperSettingsEvents
}

View file

@ -28,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshots.SnapshotStateMap
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
@ -55,6 +56,7 @@ class DeveloperSettingsPresenter @Inject constructor(
private val rageshakePresenter: RageshakePreferencesPresenter,
private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta,
private val logoutUseCase: LogoutUseCase,
) : Presenter<DeveloperSettingsState> {
@Composable
override fun present(): DeveloperSettingsState {
@ -75,6 +77,9 @@ class DeveloperSettingsPresenter @Inject constructor(
val customElementCallBaseUrl by appPreferencesStore
.getCustomElementCallBaseUrlFlow()
.collectAsState(initial = null)
val isSimplifiedSlidingSyncEnabled by appPreferencesStore
.isSimplifiedSlidingSyncEnabledFlow()
.collectAsState(initial = false)
LaunchedEffect(Unit) {
FeatureFlags.entries
@ -114,6 +119,10 @@ class DeveloperSettingsPresenter @Inject constructor(
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
}
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
is DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled -> coroutineScope.launch {
appPreferencesStore.setSimplifiedSlidingSyncEnabled(event.isEnabled)
logoutUseCase.logout(ignoreSdkError = true)
}
}
}
@ -127,6 +136,7 @@ class DeveloperSettingsPresenter @Inject constructor(
defaultUrl = ElementCallConfig.DEFAULT_BASE_URL,
validator = ::customElementCallUrlValidator,
),
isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled,
eventSink = ::handleEvents
)
}

View file

@ -27,6 +27,7 @@ data class DeveloperSettingsState(
val rageshakeState: RageshakePreferencesState,
val clearCacheAction: AsyncData<Unit>,
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
val isSimpleSlidingSyncEnabled: Boolean,
val eventSink: (DeveloperSettingsEvents) -> Unit
)

View file

@ -39,6 +39,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
fun aDeveloperSettingsState(
clearCacheAction: AsyncData<Unit> = AsyncData.Uninitialized,
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
isSimplifiedSlidingSyncEnabled: Boolean = false,
eventSink: (DeveloperSettingsEvents) -> Unit = {},
) = DeveloperSettingsState(
features = aFeatureUiModelList(),
@ -46,6 +47,7 @@ fun aDeveloperSettingsState(
cacheSize = AsyncData.Success("1.2 MB"),
clearCacheAction = clearCacheAction,
customElementCallBaseUrlState = customElementCallBaseUrlState,
isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled,
eventSink = eventSink,
)

View file

@ -26,6 +26,7 @@ import io.element.android.features.preferences.impl.R
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField
import io.element.android.libraries.designsystem.preview.ElementPreview
@ -60,6 +61,14 @@ fun DeveloperSettingsView(
title = "Configure tracing",
onClick = onOpenConfigureTracing,
)
PreferenceSwitch(
title = "Enable Simplified Sliding Sync",
subtitle = "When toggled you'll be logged out of the app and will need to log in again.",
isChecked = state.isSimpleSlidingSyncEnabled,
onCheckedChange = {
state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(it))
}
)
}
PreferenceCategory(title = "Showkase") {
PreferenceText(

View file

@ -15,9 +15,11 @@
<string name="screen_advanced_settings_share_presence">"Κοινή χρήση παρουσίας"</string>
<string name="screen_advanced_settings_share_presence_description">"Εάν απενεργοποιηθεί, δεν θα μπορείς να στέλνεις ή να λαμβάνεις αποδεικτικά ανάγνωσης ή ειδοποιήσεις πληκτρολόγησης."</string>
<string name="screen_advanced_settings_view_source_description">"Ενεργοποίησε την επιλογή για προβολή πηγής μηνυμάτων στη ροή."</string>
<string name="screen_blocked_users_empty">"Δεν έχεις αποκλεισμένους χρήστες"</string>
<string name="screen_blocked_users_unblock_alert_action">"Άρση αποκλεισμού"</string>
<string name="screen_blocked_users_unblock_alert_description">"Θα μπορείς να δεις ξανά όλα τα μηνύματα του."</string>
<string name="screen_blocked_users_unblock_alert_title">"Κατάργηση αποκλεισμού χρήστη"</string>
<string name="screen_blocked_users_unblocking">"Άρση αποκλεισμού…"</string>
<string name="screen_edit_profile_display_name">"Εμφανιζόμενο όνομα"</string>
<string name="screen_edit_profile_display_name_placeholder">"Το εμφανιζόμενο όνομά σου"</string>
<string name="screen_edit_profile_error">"Παρουσιάστηκε ένα άγνωστο σφάλμα και οι πληροφορίες δεν μπορούσαν να αλλάξουν."</string>
@ -45,7 +47,7 @@
<string name="screen_notification_settings_mentions_section_title">"Αναφορές"</string>
<string name="screen_notification_settings_mode_all">"Όλα"</string>
<string name="screen_notification_settings_mode_mentions">"Αναφορές"</string>
<string name="screen_notification_settings_notification_section_title">"Ειδοποιήσε με για"</string>
<string name="screen_notification_settings_notification_section_title">"Ειδοποίησέ με για"</string>
<string name="screen_notification_settings_room_mention_label">"Ειδοποίηση για @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Για να λαμβάνεις ειδοποιήσεις, άλλαξε το %1$s ."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"ρυθμίσεις συστήματος"</string>

View file

@ -13,7 +13,7 @@
<string name="screen_advanced_settings_send_read_receipts">"Ricevute di visualizzazione"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Se disattivato, le tue ricevute di visualizzazione non verranno inviate a nessuno. Riceverai comunque ricevute di visualizzazione da altri utenti."</string>
<string name="screen_advanced_settings_share_presence">"Condividi presenza online"</string>
<string name="screen_advanced_settings_share_presence_description">"Se disattivato, non potrai inviare o ricevere ricevute di visualizzazione o notifiche di scrittura."</string>
<string name="screen_advanced_settings_share_presence_description">"Se disattivato, non potrai inviare o ricevere ricevute di lettura o notifiche di scrittura."</string>
<string name="screen_advanced_settings_view_source_description">"Attiva l\'opzione per visualizzare il codice sorgente del messaggio nella conversazione."</string>
<string name="screen_blocked_users_empty">"Non hai utenti bloccati"</string>
<string name="screen_blocked_users_unblock_alert_action">"Sblocca"</string>

View file

@ -7,6 +7,7 @@
<string name="screen_advanced_settings_element_call_base_url_description">"დააყენეთ საბაზისო URL Element-ის ზარებისათვის."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"არასწორი URL, გთხოვთ, დარწმუნდეთ, რომ შეიტანეთ პროტოკოლი (http/https) და სწორი მისამართი."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"გამორთეთ მდიდარი ტექსტის რედაქტორი, რათა ხელით აკრიფოთ Markdown."</string>
<string name="screen_advanced_settings_view_source_description">"ჩართეთ ოპცია რათა შეტყობინების წყაროს დროის ისტორია ნახოთ."</string>
<string name="screen_blocked_users_unblock_alert_action">"განბლოკვა"</string>
<string name="screen_blocked_users_unblock_alert_description">"თქვენ კვლავ შეძლებთ მათგან ყველა შეტყობინების ნახვას."</string>
<string name="screen_blocked_users_unblock_alert_title">"Მომხმარებლის განბლოკვა"</string>
@ -32,6 +33,8 @@
<string name="screen_notification_settings_enable_notifications">"შეტყობინებების ჩართვა ამ მოწყობილობაზე"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"კონფიგურაცია არ გამოსწორებულა, გთხოვთ, კვლავ სცადოთ."</string>
<string name="screen_notification_settings_group_chats">"ჯგუფური ჩატები"</string>
<string name="screen_notification_settings_invite_for_me_label">"მოსაწვევები"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"თქვენი სახლის სერვერი არ უჭერს მხარს ამ პარამეტრს დაშიფრულ ოთახებში, ზოგიერთ ოთახში შეიძლება არ მიიღოთ შეტყობინება."</string>
<string name="screen_notification_settings_mentions_section_title">"ხსენებები"</string>
<string name="screen_notification_settings_mode_all">"ყველა"</string>
<string name="screen_notification_settings_mode_mentions">"ხსენებები"</string>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"Upewnij się, że nie pominiesz żadnego połączenia. Zmień swoje ustawienia i zezwól na powiadomienia na blokadzie ekranu."</string>
<string name="full_screen_intent_banner_title">"Popraw jakość swoich rozmów"</string>
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Wybierz sposób otrzymywania powiadomień"</string>
<string name="screen_advanced_settings_developer_mode">"Tryb dewelopera"</string>
<string name="screen_advanced_settings_developer_mode_description">"Włącz, aby uzyskać dostęp do funkcji dla deweloperów."</string>
<string name="screen_advanced_settings_element_call_base_url">"Własny bazowy URL dla połączeń Element"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Ustaw własny bazowy URL dla połączeń Element"</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Nieprawidłowy adres URL, upewnij się, że zawiera protokół (http/https) i poprawny adres."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Wyłącz edytor tekstu bogatego, aby pisać tekst Markdown ręcznie."</string>
<string name="screen_advanced_settings_view_source_description">"Włącz opcję, aby wyświetlić źródło wiadomości na osi czasu."</string>
<string name="screen_blocked_users_unblock_alert_action">"Odblokuj"</string>
<string name="screen_blocked_users_unblock_alert_description">"Będziesz mógł ponownie zobaczyć wszystkie wiadomości od tego użytkownika."</string>
<string name="screen_blocked_users_unblock_alert_title">"Odblokuj użytkownika"</string>
<string name="screen_edit_profile_display_name">"Wyświetlana nazwa"</string>
<string name="screen_edit_profile_display_name_placeholder">"Twoja wyświetlana nazwa"</string>
<string name="screen_edit_profile_error">"Wystąpił nieznany błąd przez co nie można było zmienić informacji."</string>
<string name="screen_edit_profile_error_title">"Nie można zaktualizować profilu"</string>
<string name="screen_edit_profile_title">"Edytuj profil"</string>
<string name="screen_edit_profile_updating_details">"Aktualizowanie profilu…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Dodatkowe ustawienia"</string>
<string name="screen_notification_settings_calls_label">"Połączenia audio i wideo"</string>
<string name="screen_notification_settings_configuration_mismatch">"Niezgodność konfiguracji"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Uprościliśmy Ustawienia powiadomień, aby ułatwić nawigowanie między opcjami. Niektóre ustawienia, które wybrałeś mogły zniknąć, lecz są wciąż aktywne.
Niektóre ustawienia mogą ulec zmianie, jeśli kontynuujesz."</string>
<string name="screen_notification_settings_direct_chats">"Czaty prywatne"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Ustawienia własne wybranego czatu"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Wystąpił błąd podczas aktualizacji ustawienia powiadomień."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Wszystkie wiadomości"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Tylko wzmianki i słowa kluczowe"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Na czatach prywatnych, powiadamiaj mnie przez"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Na czatach grupowych powiadamiaj mnie przez"</string>
<string name="screen_notification_settings_enable_notifications">"Włącz powiadomienia na tym urządzeniu"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfiguracja nie została poprawiona, spróbuj ponownie."</string>
<string name="screen_notification_settings_group_chats">"Czaty grupowe"</string>
<string name="screen_notification_settings_invite_for_me_label">"Zaproszenia"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Twój serwer domowy nie wspiera tej opcji w pokojach szyfrowanych, możesz nie otrzymać powiadomień z niektórych pokoi."</string>
<string name="screen_notification_settings_mentions_section_title">"Wzmianki"</string>
<string name="screen_notification_settings_mode_all">"Wszystkie"</string>
<string name="screen_notification_settings_mode_mentions">"Wzmianki"</string>
<string name="screen_notification_settings_notification_section_title">"Powiadamiaj mnie przez"</string>
<string name="screen_notification_settings_room_mention_label">"Powiadom mnie na @pokój"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Aby otrzymywać powiadomienia, zmień swoje%1$s ."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"ustawienia systemowe"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Powiadomienia systemowe wyłączone"</string>
<string name="screen_notification_settings_title">"Powiadomienia"</string>
</resources>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Escolha como receber notificações"</string>
<string name="screen_advanced_settings_developer_mode">"Modo de desenvolvedor"</string>
<string name="screen_advanced_settings_developer_mode_description">"Habilite para ter acesso a recursos e funcionalidades para desenvolvedores."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Desative o editor de rich text para digitar Markdown manualmente."</string>
<string name="screen_blocked_users_unblock_alert_action">"Desbloquear"</string>
<string name="screen_blocked_users_unblock_alert_description">"Você poderá ver todas as mensagens deles novamente."</string>
<string name="screen_blocked_users_unblock_alert_title">"Desbloquear usuário"</string>
<string name="screen_edit_profile_display_name">"Nome de exibição"</string>
<string name="screen_edit_profile_display_name_placeholder">"Seu nome de exibição"</string>
<string name="screen_edit_profile_error">"Um erro desconhecido foi encontrado e as informações não puderam ser alteradas."</string>
<string name="screen_edit_profile_error_title">"Não foi possível atualizar o perfil"</string>
<string name="screen_edit_profile_title">"Editar perfil"</string>
<string name="screen_edit_profile_updating_details">"Atualizando o perfil…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Configurações adicionais"</string>
<string name="screen_notification_settings_calls_label">"Chamadas de áudio e vídeo"</string>
<string name="screen_notification_settings_configuration_mismatch">"Incompatibilidade de configuração"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Simplificamos as configurações de notificações para facilitar a localização das opções. Algumas configurações personalizadas que você escolheu no passado não são mostradas aqui, mas ainda estão ativas.
Se você continuar, algumas de suas configurações poderão mudar."</string>
<string name="screen_notification_settings_direct_chats">"Conversas privadas"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Configuração personalizada por chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Ocorreu um erro ao atualizar a configuração de notificação."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Todas as mensagens"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Somente menções e palavras-chave"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Em conversas privadas, me notifique para"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Em conversas em grupos, me notifique para"</string>
<string name="screen_notification_settings_enable_notifications">"Ativar notificações neste dispositivo"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"A configuração não foi corrigida, tente novamente."</string>
<string name="screen_notification_settings_group_chats">"Bate-papos em grupo"</string>
<string name="screen_notification_settings_mentions_section_title">"Menções"</string>
<string name="screen_notification_settings_mode_all">"Todos"</string>
<string name="screen_notification_settings_mode_mentions">"Menções"</string>
<string name="screen_notification_settings_notification_section_title">"Me notifique para"</string>
<string name="screen_notification_settings_room_mention_label">"Notifique-me em @room"</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"configurações do sistema"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Notificações do sistema desativadas"</string>
<string name="screen_notification_settings_title">"Notificações"</string>
</resources>

View file

@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.logout.test.FakeLogoutUseCase
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
@ -35,6 +36,8 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -55,6 +58,7 @@ class DeveloperSettingsPresenterTest {
assertThat(initialState.cacheSize).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.customElementCallBaseUrlState).isNotNull()
assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
val loadedState = awaitItem()
assertThat(loadedState.rageshakeState.isEnabled).isFalse()
assertThat(loadedState.rageshakeState.isSupported).isTrue()
@ -160,6 +164,30 @@ class DeveloperSettingsPresenterTest {
}
}
@Test
fun `present - toggling simplified sliding sync changes the preferences and logs out the user`() = runTest {
val logoutCallRecorder = lambdaRecorder<Boolean, String> { "" }
val logoutUseCase = FakeLogoutUseCase(logoutLambda = logoutCallRecorder)
val preferences = InMemoryAppPreferencesStore()
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true))
assertThat(awaitItem().isSimpleSlidingSyncEnabled).isTrue()
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
logoutCallRecorder.assertions().isCalledOnce()
initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false))
assertThat(awaitItem().isSimpleSlidingSyncEnabled).isFalse()
assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
logoutCallRecorder.assertions().isCalledExactly(times = 2)
}
}
private fun createDeveloperSettingsPresenter(
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
@ -167,6 +195,7 @@ class DeveloperSettingsPresenterTest {
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
buildMeta: BuildMeta = aBuildMeta(),
logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" })
): DeveloperSettingsPresenter {
return DeveloperSettingsPresenter(
featureFlagService = featureFlagService,
@ -175,6 +204,7 @@ class DeveloperSettingsPresenterTest {
rageshakePresenter = rageshakePresenter,
appPreferencesStore = preferencesStore,
buildMeta = buildMeta,
logoutUseCase = logoutUseCase,
)
}
}

View file

@ -109,6 +109,19 @@ class DeveloperSettingsViewTest {
rule.onNodeWithText("Clear cache").performClick()
eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache)
}
@Config(qualifiers = "h1500dp")
@Test
fun `clicking on the simplified sliding sync switch emits the expected event`() {
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>()
rule.setDeveloperSettingsView(
state = aDeveloperSettingsState(
eventSink = eventsRecorder
),
)
rule.onNodeWithText("Enable Simplified Sliding Sync").performClick()
eventsRecorder.assertSingle(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeveloperSettingsView(

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crash_detection_dialog_content">"%1$s uległ awarii podczas ostatniego użycia. Czy chcesz przesłać nam raport o awarii?"</string>
<string name="rageshake_detection_dialog_content">"Wygląda na to, że potrząsasz telefonem z frustracji. Czy chcesz otworzyć ekran zgłaszania błędów?"</string>
<string name="settings_rageshake">"Gniewne wstrząsanie"</string>
<string name="settings_rageshake_detection_threshold">"Próg wykrywania"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crash_detection_dialog_content">"%1$s fechou inesperadamente na última vez que foi usado. Gostaria de compartilhar um relatório de falhas conosco?"</string>
<string name="rageshake_detection_dialog_content">"Você parece estar sacudindo o telefone em sinal de frustração. Você gostaria de abrir a tela de relatório de erros?"</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Limiar de deteção"</string>
</resources>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_bug_report_attach_screenshot">"Dołącz zrzut ekranu"</string>
<string name="screen_bug_report_contact_me">"Możecie skontaktować się ze mną, jeśli macie jakiekolwiek dodatkowe pytania."</string>
<string name="screen_bug_report_contact_me_title">"Napisz do mnie"</string>
<string name="screen_bug_report_edit_screenshot">"Edytuj zrzut ekranu"</string>
<string name="screen_bug_report_editor_description">"Opisz problem. Co zrobiłeś? Czego oczekiwałeś? Co się stało zamiast tego. Podaj jak najwięcej szczegółów."</string>
<string name="screen_bug_report_editor_placeholder">"Opisz problem…"</string>
<string name="screen_bug_report_editor_supporting">"Jeśli to możliwe, napisz zgłoszenje w języku angielskim."</string>
<string name="screen_bug_report_include_crash_logs">"Wyślij logi awarii"</string>
<string name="screen_bug_report_include_logs">"Zezwól na logi"</string>
<string name="screen_bug_report_include_screenshot">"Wyślij zrzut ekranu"</string>
<string name="screen_bug_report_logs_description">"Logi zostaną dołączone do Twojej wiadomości, aby upewnić się, że wszystko działa poprawnie. Aby wysłać wiadomość bez logów, wyłącz to ustawienie."</string>
<string name="screen_bug_report_rash_logs_alert_title">"%1$s uległ awarii podczas ostatniego użycia. Czy chcesz przesłać nam raport o awarii?"</string>
<string name="screen_bug_report_view_logs">"Wyświetl logi"</string>
</resources>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_bug_report_attach_screenshot">"Anexar captura de tela"</string>
<string name="screen_bug_report_contact_me">"Você pode entrar em contato comigo se tiver alguma pergunta adicional."</string>
<string name="screen_bug_report_contact_me_title">"Entre em contato comigo"</string>
<string name="screen_bug_report_edit_screenshot">"Editar captura de tela"</string>
<string name="screen_bug_report_editor_description">"Descreva o problema. O que você fez? O que você esperava que acontecesse? O que realmente aconteceu? Por favor, forneça o máximo de detalhes possível."</string>
<string name="screen_bug_report_editor_placeholder">"Descreva o problema…"</string>
<string name="screen_bug_report_editor_supporting">"Se possível, escreva a descrição em inglês."</string>
<string name="screen_bug_report_include_crash_logs">"Enviar registros de falhas"</string>
<string name="screen_bug_report_include_logs">"Permitir registros"</string>
<string name="screen_bug_report_include_screenshot">"Enviar captura de tela"</string>
<string name="screen_bug_report_logs_description">"Os registros serão incluídos com sua mensagem para garantir que tudo esteja funcionando corretamente. Para enviar sua mensagem sem registros, desative essa configuração."</string>
<string name="screen_bug_report_rash_logs_alert_title">"%1$s fechou inesperadamente na última vez que foi usado. Gostaria de compartilhar um relatório de falhas conosco?"</string>
</resources>

View file

@ -4,6 +4,7 @@
<string name="screen_notification_settings_mentions_only_disclaimer">"Ο οικιακός διακομιστής σου δεν υποστηρίζει αυτήν την επιλογή σε κρυπτογραφημένα δωμάτια, ενδέχεται να μην λάβεις ειδοποίηση σε ορισμένα δωμάτια."</string>
<string name="screen_polls_history_title">"Δημοσκοπήσεις"</string>
<string name="screen_room_change_permissions_administrators">"Μόνο διαχειριστές"</string>
<string name="screen_room_change_permissions_ban_people">"Αποκλεισμός ατόμων"</string>
<string name="screen_room_change_permissions_delete_messages">"Αφαίρεση μηνυμάτων"</string>
<string name="screen_room_change_permissions_everyone">"Όλοι"</string>
<string name="screen_room_change_permissions_invite_people">"Πρόσκληση ατόμων"</string>
@ -58,17 +59,23 @@
<string name="screen_room_details_title">"Πληροφορίες δωματίου"</string>
<string name="screen_room_details_topic_title">"Θέμα"</string>
<string name="screen_room_details_updating_room">"Ενημέρωση δωματίου…"</string>
<string name="screen_room_member_list_ban_member_confirmation_action">"Αποκλεισμός"</string>
<string name="screen_room_member_list_ban_member_confirmation_description">"Δεν θα μπορεί να συμμετέχει ξανά σε αυτό το δωμάτιο εάν προσκληθεί."</string>
<string name="screen_room_member_list_ban_member_confirmation_title">"Θες σίγουρα να αποκλείσεις αυτό το μέλος;"</string>
<string name="screen_room_member_list_banned_empty">"Δεν υπάρχουν αποκλεισμένοι χρήστες σε αυτό το δωμάτιο."</string>
<string name="screen_room_member_list_banning_user">"Αποκλεισμός του χρήστη %1$s"</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d άτομο"</item>
<item quantity="other">"%1$d άτομα"</item>
</plurals>
<string name="screen_room_member_list_manage_member_ban">"Αφαίρεση και αποκλεισμός μέλους"</string>
<string name="screen_room_member_list_manage_member_remove">"Αφαίρεση από το δωμάτιο"</string>
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Αφαίρεση και αποκλεισμός μέλους"</string>
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Μόνο αφαίρεση μέλους"</string>
<string name="screen_room_member_list_manage_member_remove_confirmation_title">"Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;"</string>
<string name="screen_room_member_list_manage_member_unban_action">"Αναίρεση αποκλεισμού"</string>
<string name="screen_room_member_list_manage_member_unban_message">"Θα μπορεί να συμμετάσχει ξανά στο δωμάτιο εάν προσκληθεί."</string>
<string name="screen_room_member_list_manage_member_unban_title">"Άρση αποκλεισμού χρήστη"</string>
<string name="screen_room_member_list_manage_member_user_info">"Προβολή προφίλ"</string>
<string name="screen_room_member_list_mode_banned">"Αποκλεισμένοι"</string>
<string name="screen_room_member_list_mode_members">"Μέλη"</string>
@ -77,6 +84,7 @@
<string name="screen_room_member_list_role_administrator">"Διαχειριστής"</string>
<string name="screen_room_member_list_role_moderator">"Συντονιστής"</string>
<string name="screen_room_member_list_room_members_header_title">"Μέλη δωματίου"</string>
<string name="screen_room_member_list_unbanning_user">"Άρση αποκλεισμού %1$s"</string>
<string name="screen_room_notification_settings_allow_custom">"Να επιτρέπεται η προσαρμοσμένη ρύθμιση"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Η ενεργοποίηση αυτής της ρύθμισης θα παρακάμψει την προεπιλεγμένη ρύθμιση"</string>
<string name="screen_room_notification_settings_custom_settings_title">"Ειδοποιήσε με σε αυτήν τη συνομιλία για"</string>

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_settings_edit_failed_updating_default_mode">"შეტყობინებების პარამეტრის განახლებისას მოხდა შეცდომა."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"თქვენი სახლის სერვერი არ უჭერს მხარს ამ პარამეტრს დაშიფრულ ოთახებში, ზოგიერთ ოთახში შეიძლება არ მიიღოთ შეტყობინება."</string>
<string name="screen_polls_history_title">"გამოკითხვები"</string>
<string name="screen_room_change_permissions_everyone">"ყველა"</string>
<string name="screen_room_details_add_topic_title">"თემის დამატება"</string>
<string name="screen_room_details_already_a_member">"უკვე წევრია"</string>
@ -39,6 +41,7 @@
<string name="screen_room_notification_settings_error_loading_settings">"შეტყობინებების პარამეტრების ჩატვირთვისას მოხდა შეცდომა."</string>
<string name="screen_room_notification_settings_error_restoring_default">"ნაგულისხმევი რეჟიმის აღდგენა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_room_notification_settings_error_setting_mode">"რეჟიმის დაყენება ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_room_notification_settings_mentions_only_disclaimer">"თქვენი სახლის სერვერი არ უჭერს მხარს ამ პარამეტრს დაშიფრულ ოთახებში, თქვენ არ მიიღებთ შეტყობინებას ამ ოთახში."</string>
<string name="screen_room_notification_settings_mode_all_messages">"ყველა შეტყობინება"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"მხოლოდ ხსენებები და საკვანძო სიტყვები"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"ამ ოთახში, შემატყობინეთ:"</string>

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Wystąpił błąd podczas aktualizacji ustawienia powiadomień."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Twój serwer domowy nie wspiera tej opcji w pokojach szyfrowanych, możesz nie otrzymać powiadomień z niektórych pokoi."</string>
<string name="screen_polls_history_title">"Ankiety"</string>
<string name="screen_room_change_permissions_everyone">"Wszyscy"</string>
<string name="screen_room_details_add_topic_title">"Dodaj temat"</string>
<string name="screen_room_details_already_a_member">"Jest już członkiem"</string>
<string name="screen_room_details_already_invited">"Już zaproszony"</string>
<string name="screen_room_details_edit_room_title">"Edytuj pokój"</string>
<string name="screen_room_details_edition_error">"Wystąpił nieznany błąd i nie można było zmienić informacji."</string>
<string name="screen_room_details_edition_error_title">"Nie można zaktualizować pokoju"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Wiadomości są zabezpieczone kłódkami. Tylko Ty i odbiorcy macie unikalne klucze do ich odblokowania."</string>
<string name="screen_room_details_encryption_enabled_title">"Szyfrowanie wiadomości włączone"</string>
<string name="screen_room_details_error_loading_notification_settings">"Wystąpił błąd podczas ładowania ustawień powiadomień."</string>
<string name="screen_room_details_error_muting">"Wyciszenie tego pokoju nie powiodło się, spróbuj ponownie."</string>
<string name="screen_room_details_error_unmuting">"Nie udało się wyłączyć wyciszenia tego pokoju. Spróbuj ponownie."</string>
<string name="screen_room_details_invite_people_title">"Zaproś znajomych"</string>
<string name="screen_room_details_leave_conversation_title">"Opuść rozmowę"</string>
<string name="screen_room_details_leave_room_title">"Opuść pokój"</string>
<string name="screen_room_details_notification_mode_custom">"Niestandardowy"</string>
<string name="screen_room_details_notification_mode_default">"Domyślny"</string>
<string name="screen_room_details_notification_title">"Powiadomienia"</string>
<string name="screen_room_details_room_name_label">"Nazwa pokoju"</string>
<string name="screen_room_details_security_title">"Bezpieczeństwo"</string>
<string name="screen_room_details_share_room_title">"Udostępnij pokój"</string>
<string name="screen_room_details_topic_title">"Temat"</string>
<string name="screen_room_details_updating_room">"Aktualizuję pokój…"</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d osoba"</item>
<item quantity="few">"%1$d osoby"</item>
<item quantity="many">"%1$d osób"</item>
</plurals>
<string name="screen_room_member_list_pending_header_title">"Oczekiwanie"</string>
<string name="screen_room_member_list_room_members_header_title">"Członkowie pokoju"</string>
<string name="screen_room_notification_settings_allow_custom">"Zezwalaj na ustawienia niestandardowe"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Włączenie tej opcji nadpisze ustawienie domyślne"</string>
<string name="screen_room_notification_settings_custom_settings_title">"Powiadamiaj mnie o tym czacie przez"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Możesz to zmienić w swoim %1$s."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"ustawienia globalne"</string>
<string name="screen_room_notification_settings_default_setting_title">"Ustawienie domyślne"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Usuń ustawienia własne"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Wystąpił błąd podczas ładowania ustawień powiadomień."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Nie udało się przywrócić trybu domyślnego, spróbuj ponownie."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Nie udało się ustawić trybu, spróbuj ponownie."</string>
<string name="screen_room_notification_settings_mentions_only_disclaimer">"Twój serwer domowy nie wspiera tej opcji w pokojach szyfrowanych, możesz nie otrzymać powiadomień z tego pokoju."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Wszystkie wiadomości"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Tylko wzmianki i słowa kluczowe"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"W tym pokoju, powiadamiaj mnie przez"</string>
</resources>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Ocorreu um erro ao atualizar a configuração de notificação."</string>
<string name="screen_room_change_permissions_everyone">"Todos"</string>
<string name="screen_room_details_add_topic_title">"Adicionar tópico"</string>
<string name="screen_room_details_already_a_member">"Já é membro"</string>
<string name="screen_room_details_already_invited">"Já foi convidado"</string>
<string name="screen_room_details_edit_room_title">"Editar sala"</string>
<string name="screen_room_details_edition_error">"Ocorreu um erro desconhecido e as informações não puderam ser alteradas."</string>
<string name="screen_room_details_edition_error_title">"Não foi possível atualizar a sala"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"As mensagens são protegidas com bloqueios. Somente você e os destinatários têm as chaves exclusivas para desbloqueá-los."</string>
<string name="screen_room_details_encryption_enabled_title">"Criptografia de mensagens ativada"</string>
<string name="screen_room_details_error_loading_notification_settings">"Ocorreu um erro ao carregar as configurações de notificação."</string>
<string name="screen_room_details_error_muting">"Falha ao silenciar esta sala, tente novamente."</string>
<string name="screen_room_details_error_unmuting">"Falha ao ativar o som desta sala. Tente novamente."</string>
<string name="screen_room_details_invite_people_title">"Convidar pessoas"</string>
<string name="screen_room_details_leave_conversation_title">"Sair da conversa"</string>
<string name="screen_room_details_leave_room_title">"Sair da sala"</string>
<string name="screen_room_details_notification_mode_custom">"Personalizado"</string>
<string name="screen_room_details_notification_mode_default">"Padrão"</string>
<string name="screen_room_details_notification_title">"Notificações"</string>
<string name="screen_room_details_room_name_label">"Nome da sala"</string>
<string name="screen_room_details_security_title">"Segurança"</string>
<string name="screen_room_details_share_room_title">"Compartilhar sala"</string>
<string name="screen_room_details_topic_title">"Tópico"</string>
<string name="screen_room_details_updating_room">"Atualizando a sala…"</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d pessoa"</item>
<item quantity="other">"%1$d pessoas"</item>
</plurals>
<string name="screen_room_member_list_pending_header_title">"Pendente"</string>
<string name="screen_room_member_list_room_members_header_title">"Membros da sala"</string>
<string name="screen_room_notification_settings_allow_custom">"Permitir configuração personalizada"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"Ativar isso substituirá sua configuração padrão"</string>
<string name="screen_room_notification_settings_custom_settings_title">"Me notifique nesta conversa para"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Você pode alterá-lo no seu %1$s."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"configurações globais"</string>
<string name="screen_room_notification_settings_default_setting_title">"Configuração padrão"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Remover configuração personalizada"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Ocorreu um erro ao carregar as configurações de notificação."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Falha ao restaurar o modo padrão, tente novamente."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Falha ao definir o modo, tente novamente."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Todas as mensagens"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Somente menções e palavras-chave"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"Nesta sala, notifique-me para"</string>
</resources>

View file

@ -17,11 +17,15 @@
package io.element.android.features.roomdetails
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.lambda.lambdaError
fun aMatrixRoom(
roomId: RoomId = A_ROOM_ID,
@ -34,6 +38,16 @@ fun aMatrixRoom(
isDirect: Boolean = false,
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
emitRoomInfo: Boolean = false,
canInviteResult: (UserId) -> Result<Boolean> = { lambdaError() },
canSendStateResult: (UserId, StateEventType) -> Result<Boolean> = { _, _ -> lambdaError() },
userDisplayNameResult: () -> Result<String?> = { lambdaError() },
userAvatarUrlResult: () -> Result<String?> = { lambdaError() },
setNameResult: (String) -> Result<Unit> = { lambdaError() },
setTopicResult: (String) -> Result<Unit> = { lambdaError() },
updateAvatarResult: (String, ByteArray) -> Result<Unit> = { _, _ -> lambdaError() },
removeAvatarResult: () -> Result<Unit> = { lambdaError() },
canUserJoinCallResult: (UserId) -> Result<Boolean> = { lambdaError() },
getUpdatedMemberResult: (UserId) -> Result<RoomMember> = { lambdaError() },
) = FakeMatrixRoom(
roomId = roomId,
displayName = displayName,
@ -42,7 +56,17 @@ fun aMatrixRoom(
isEncrypted = isEncrypted,
isPublic = isPublic,
isDirect = isDirect,
notificationSettingsService = notificationSettingsService
notificationSettingsService = notificationSettingsService,
canInviteResult = canInviteResult,
canSendStateResult = canSendStateResult,
userDisplayNameResult = userDisplayNameResult,
userAvatarUrlResult = userAvatarUrlResult,
setNameResult = setNameResult,
setTopicResult = setTopicResult,
updateAvatarResult = updateAvatarResult,
removeAvatarResult = removeAvatarResult,
canUserJoinCallResult = canUserJoinCallResult,
getUpdatedMemberResult = getUpdatedMemberResult,
).apply {
if (emitRoomInfo) {
givenRoomInfo(

View file

@ -53,6 +53,9 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.FakeLifecycleOwner
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.element.android.tests.testutils.withFakeLifecycleOwner
import kotlinx.collections.immutable.persistentListOf
@ -110,7 +113,11 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state is created from room if roomInfo is null`() = runTest {
val room = aMatrixRoom()
val room = aMatrixRoom(
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room)
presenter.test {
val initialState = awaitItem()
@ -128,7 +135,11 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state is updated with roomInfo if it exists`() = runTest {
val roomInfo = aRoomInfo(name = "A room name", topic = "A topic", avatarUrl = "https://matrix.org/avatar.jpg")
val room = aMatrixRoom().apply {
val room = aMatrixRoom(
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
).apply {
givenRoomInfo(roomInfo)
}
val presenter = createRoomDetailsPresenter(room)
@ -145,7 +156,12 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state with no room name`() = runTest {
val room = aMatrixRoom(displayName = "")
val room = aMatrixRoom(
displayName = "",
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room)
presenter.test {
val initialState = awaitItem()
@ -162,6 +178,16 @@ class RoomDetailsPresenterTest {
val room = aMatrixRoom(
isEncrypted = true,
isDirect = true,
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
getUpdatedMemberResult = { userId ->
when (userId) {
A_SESSION_ID -> Result.success(myRoomMember)
A_USER_ID_2 -> Result.success(otherRoomMember)
else -> lambdaError()
}
},
).apply {
val roomMembers = persistentListOf(myRoomMember, otherRoomMember)
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
@ -181,9 +207,11 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state when user can invite others to room`() = runTest {
val room = aMatrixRoom().apply {
givenCanInviteResult(Result.success(true))
}
val room = aMatrixRoom(
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers())
presenter.test {
// Initially false
@ -197,9 +225,11 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state when user can not invite others to room`() = runTest {
val room = aMatrixRoom().apply {
givenCanInviteResult(Result.success(false))
}
val room = aMatrixRoom(
canInviteResult = { Result.success(false) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room)
presenter.test {
assertThat(awaitItem().canInvite).isFalse()
@ -210,9 +240,11 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state when canInvite errors`() = runTest {
val room = aMatrixRoom().apply {
givenCanInviteResult(Result.failure(Throwable("Whoops")))
}
val room = aMatrixRoom(
canInviteResult = { Result.failure(Throwable("Whoops")) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room)
presenter.test {
assertThat(awaitItem().canInvite).isFalse()
@ -223,12 +255,18 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state when user can edit one attribute`() = runTest {
val room = aMatrixRoom().apply {
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(false))
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Whelp")))
givenCanInviteResult(Result.success(false))
}
val room = aMatrixRoom(
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_TOPIC -> Result.success(true)
StateEventType.ROOM_NAME -> Result.success(false)
StateEventType.ROOM_AVATAR -> Result.failure(Throwable("Whelp"))
else -> lambdaError()
}
},
canInviteResult = { Result.success(false) },
canUserJoinCallResult = { Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room)
presenter.test {
// Initially false
@ -247,14 +285,26 @@ class RoomDetailsPresenterTest {
val room = aMatrixRoom(
isEncrypted = true,
isDirect = true,
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_TOPIC -> Result.success(true)
StateEventType.ROOM_NAME -> Result.success(true)
StateEventType.ROOM_AVATAR -> Result.success(true)
else -> lambdaError()
}
},
canInviteResult = { Result.success(false) },
canUserJoinCallResult = { Result.success(true) },
getUpdatedMemberResult = { userId ->
when (userId) {
A_SESSION_ID -> Result.success(myRoomMember)
A_USER_ID_2 -> Result.success(otherRoomMember)
else -> lambdaError()
}
},
).apply {
val roomMembers = persistentListOf(myRoomMember, otherRoomMember)
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(true))
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true))
givenCanInviteResult(Result.success(false))
}
val presenter = createRoomDetailsPresenter(room)
presenter.test {
@ -278,12 +328,28 @@ class RoomDetailsPresenterTest {
isEncrypted = true,
isDirect = true,
topic = null,
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_AVATAR,
StateEventType.ROOM_TOPIC,
StateEventType.ROOM_NAME -> Result.success(true)
else -> lambdaError()
}
},
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
getUpdatedMemberResult = { userId ->
when (userId) {
A_SESSION_ID -> Result.success(myRoomMember)
A_USER_ID_2 -> Result.success(otherRoomMember)
else -> lambdaError()
}
},
).apply {
val roomMembers = persistentListOf(myRoomMember, otherRoomMember)
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
}
val presenter = createRoomDetailsPresenter(room)
presenter.test {
skipItems(1)
@ -297,12 +363,20 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state when user can edit all attributes`() = runTest {
val room = aMatrixRoom().apply {
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(true))
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true))
givenCanInviteResult(Result.success(false))
}
val room = aMatrixRoom(
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_TOPIC -> Result.success(true)
StateEventType.ROOM_NAME -> Result.success(true)
StateEventType.ROOM_AVATAR -> Result.success(true)
else -> lambdaError()
}
},
canInviteResult = {
Result.success(false)
},
canUserJoinCallResult = { Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room)
presenter.test {
// Initially false
@ -316,12 +390,20 @@ class RoomDetailsPresenterTest {
@Test
fun `present - initial state when user can edit no attributes`() = runTest {
val room = aMatrixRoom().apply {
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(false))
givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(false))
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false))
givenCanInviteResult(Result.success(false))
}
val room = aMatrixRoom(
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_TOPIC -> Result.success(false)
StateEventType.ROOM_NAME -> Result.success(false)
StateEventType.ROOM_AVATAR -> Result.success(false)
else -> lambdaError()
}
},
canInviteResult = {
Result.success(false)
},
canUserJoinCallResult = { Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room)
presenter.test {
// Initially false, and no further events
@ -333,11 +415,21 @@ class RoomDetailsPresenterTest {
@Test
fun `present - topic state is hidden when no topic and user has no permission`() = runTest {
val room = aMatrixRoom(topic = null).apply {
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(false))
givenCanInviteResult(Result.success(false))
}
val room = aMatrixRoom(
topic = null,
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_AVATAR,
StateEventType.ROOM_NAME -> Result.success(true)
StateEventType.ROOM_TOPIC -> Result.success(false)
else -> lambdaError()
}
},
canInviteResult = {
Result.success(false)
},
canUserJoinCallResult = { Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room)
presenter.test {
// The initial state is "hidden" and no further state changes happen
@ -349,12 +441,23 @@ class RoomDetailsPresenterTest {
@Test
fun `present - topic state is 'can add topic' when no topic and user has permission`() = runTest {
val room = aMatrixRoom(topic = null).apply {
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
givenCanInviteResult(Result.success(false))
val room = aMatrixRoom(
topic = null,
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_AVATAR,
StateEventType.ROOM_TOPIC,
StateEventType.ROOM_NAME -> Result.success(true)
else -> lambdaError()
}
},
canInviteResult = {
Result.success(false)
},
canUserJoinCallResult = { Result.success(true) },
).apply {
givenRoomInfo(aRoomInfo(topic = null))
}
val presenter = createRoomDetailsPresenter(room)
presenter.test {
// Ignore the initial state
@ -370,7 +473,11 @@ class RoomDetailsPresenterTest {
@Test
fun `present - leave room event is passed on to leave room presenter`() = runTest {
val leaveRoomPresenter = FakeLeaveRoomPresenter()
val room = aMatrixRoom()
val room = aMatrixRoom(
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(
room = room,
leaveRoomPresenter = leaveRoomPresenter,
@ -379,7 +486,11 @@ class RoomDetailsPresenterTest {
presenter.test {
awaitItem().eventSink(RoomDetailsEvent.LeaveRoom)
assertThat(leaveRoomPresenter.events).contains(LeaveRoomEvent.ShowConfirmation(room.roomId))
assertThat(leaveRoomPresenter.events).contains(
LeaveRoomEvent.ShowConfirmation(
room.roomId
)
)
cancelAndIgnoreRemainingEvents()
}
@ -389,33 +500,54 @@ class RoomDetailsPresenterTest {
fun `present - notification mode changes`() = runTest {
val leaveRoomPresenter = FakeLeaveRoomPresenter()
val notificationSettingsService = FakeNotificationSettingsService()
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
val room = aMatrixRoom(
notificationSettingsService = notificationSettingsService,
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(
room = room,
leaveRoomPresenter = leaveRoomPresenter,
notificationSettingsService = notificationSettingsService,
)
presenter.test {
notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
notificationSettingsService.setRoomNotificationMode(
room.roomId,
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
)
val updatedState = consumeItemsUntilPredicate {
it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
}.last()
assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - mute room notifications`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
val notificationSettingsService =
FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
val room = aMatrixRoom(
notificationSettingsService = notificationSettingsService,
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(
room = room,
notificationSettingsService = notificationSettingsService
)
presenter.test {
awaitItem().eventSink(RoomDetailsEvent.MuteNotification)
val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) {
it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE
}.last()
assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MUTE)
assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(
RoomNotificationMode.MUTE
)
cancelAndIgnoreRemainingEvents()
}
}
@ -426,29 +558,50 @@ class RoomDetailsPresenterTest {
initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
initialEncryptedGroupDefaultMode = RoomNotificationMode.ALL_MESSAGES
)
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService)
val room = aMatrixRoom(
notificationSettingsService = notificationSettingsService,
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(
room = room,
notificationSettingsService = notificationSettingsService
)
presenter.test {
awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification)
val updatedState = consumeItemsUntilPredicate {
it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES
}.last()
assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.ALL_MESSAGES)
assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(
RoomNotificationMode.ALL_MESSAGES
)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - when set is favorite event is emitted, then the action is called`() = runTest {
val room = FakeMatrixRoom()
val setIsFavoriteResult = lambdaRecorder<Boolean, Result<Unit>> { _ -> Result.success(Unit) }
val room = FakeMatrixRoom(
setIsFavoriteResult = setIsFavoriteResult,
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val analyticsService = FakeAnalyticsService()
val presenter = createRoomDetailsPresenter(room = room, analyticsService = analyticsService)
val presenter =
createRoomDetailsPresenter(room = room, analyticsService = analyticsService)
presenter.test {
val initialState = awaitItem()
initialState.eventSink(RoomDetailsEvent.SetFavorite(true))
assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true))
setIsFavoriteResult.assertions().isCalledOnce().with(value(true))
initialState.eventSink(RoomDetailsEvent.SetFavorite(false))
assertThat(room.setIsFavoriteCalls).isEqualTo(listOf(true, false))
setIsFavoriteResult.assertions().isCalledExactly(2)
.withSequence(
listOf(value(true)),
listOf(value(false)),
)
assertThat(analyticsService.capturedEvents).containsExactly(
Interaction(name = Interaction.Name.MobileRoomFavouriteToggle),
Interaction(name = Interaction.Name.MobileRoomFavouriteToggle)
@ -459,7 +612,11 @@ class RoomDetailsPresenterTest {
@Test
fun `present - changes in room info updates the is favorite flag`() = runTest {
val room = aMatrixRoom()
val room = aMatrixRoom(
canInviteResult = { Result.success(true) },
canUserJoinCallResult = { Result.success(true) },
canSendStateResult = { _, _ -> Result.success(true) },
)
val presenter = createRoomDetailsPresenter(room = room)
presenter.test {
room.givenRoomInfo(aRoomInfo(isFavorite = true))

View file

@ -39,6 +39,9 @@ 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.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
@ -98,6 +101,7 @@ class RoomDetailsEditPresenterTest {
displayName = A_ROOM_NAME,
rawName = A_ROOM_RAW_NAME,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) }
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
@ -120,11 +124,17 @@ class RoomDetailsEditPresenterTest {
@Test
fun `present - sets canChangeName if user has permission`() = runTest {
val room = aMatrixRoom(avatarUrl = AN_AVATAR_URL).apply {
givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(true))
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false))
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.failure(Throwable("Oops")))
}
val room = aMatrixRoom(
avatarUrl = AN_AVATAR_URL,
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_NAME -> Result.success(true)
StateEventType.ROOM_AVATAR -> Result.success(false)
StateEventType.ROOM_TOPIC -> Result.failure(Throwable("Oops"))
else -> lambdaError()
}
},
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -144,11 +154,17 @@ class RoomDetailsEditPresenterTest {
@Test
fun `present - sets canChangeAvatar if user has permission`() = runTest {
val room = aMatrixRoom(avatarUrl = AN_AVATAR_URL).apply {
givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(false))
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true))
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.failure(Throwable("Oops")))
}
val room = aMatrixRoom(
avatarUrl = AN_AVATAR_URL,
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_NAME -> Result.success(false)
StateEventType.ROOM_AVATAR -> Result.success(true)
StateEventType.ROOM_TOPIC -> Result.failure(Throwable("Oops"))
else -> lambdaError()
}
}
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -168,11 +184,17 @@ class RoomDetailsEditPresenterTest {
@Test
fun `present - sets canChangeTopic if user has permission`() = runTest {
val room = aMatrixRoom(avatarUrl = AN_AVATAR_URL).apply {
givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(false))
givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Oops")))
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
}
val room = aMatrixRoom(
avatarUrl = AN_AVATAR_URL,
canSendStateResult = { _, stateEventType ->
when (stateEventType) {
StateEventType.ROOM_NAME -> Result.success(false)
StateEventType.ROOM_AVATAR -> Result.failure(Throwable("Oops"))
StateEventType.ROOM_TOPIC -> Result.success(true)
else -> lambdaError()
}
}
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -197,6 +219,7 @@ class RoomDetailsEditPresenterTest {
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) }
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
@ -240,6 +263,7 @@ class RoomDetailsEditPresenterTest {
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) }
)
fakePickerProvider.givenResult(anotherAvatarUri)
val presenter = createRoomDetailsEditPresenter(room)
@ -262,6 +286,7 @@ class RoomDetailsEditPresenterTest {
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) }
)
fakePickerProvider.givenResult(anotherAvatarUri)
val fakePermissionsPresenter = FakePermissionsPresenter()
@ -298,6 +323,7 @@ class RoomDetailsEditPresenterTest {
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) }
)
fakePickerProvider.givenResult(roomAvatarUri)
val presenter = createRoomDetailsEditPresenter(room)
@ -346,6 +372,7 @@ class RoomDetailsEditPresenterTest {
displayName = "fallback",
avatarUrl = null,
emitRoomInfo = true,
canSendStateResult = { _, _ -> Result.success(true) }
)
fakePickerProvider.givenResult(roomAvatarUri)
val presenter = createRoomDetailsEditPresenter(room)
@ -389,11 +416,18 @@ class RoomDetailsEditPresenterTest {
@Test
fun `present - save changes room details if different`() = runTest {
val setNameResult = lambdaRecorder { _: String -> Result.success(Unit) }
val setTopicResult = lambdaRecorder { _: String -> Result.success(Unit) }
val removeAvatarResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val room = aMatrixRoom(
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
setNameResult = setNameResult,
setTopicResult = setTopicResult,
removeAvatarResult = removeAvatarResult,
canSendStateResult = { _, _ -> Result.success(true) }
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
@ -405,16 +439,20 @@ class RoomDetailsEditPresenterTest {
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
initialState.eventSink(RoomDetailsEditEvents.Save)
skipItems(5)
assertThat(room.newName).isEqualTo("New name")
assertThat(room.newTopic).isEqualTo("New topic")
assertThat(room.newAvatarData).isNull()
assertThat(room.removedAvatar).isTrue()
setNameResult.assertions().isCalledOnce().with(value("New name"))
setTopicResult.assertions().isCalledOnce().with(value("New topic"))
removeAvatarResult.assertions().isCalledOnce()
}
}
@Test
fun `present - save doesn't change room details if they're the same trimmed`() = runTest {
val room = aMatrixRoom(topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL)
val room = aMatrixRoom(
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
canSendStateResult = { _, _ -> Result.success(true) }
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -423,17 +461,18 @@ class RoomDetailsEditPresenterTest {
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName(" Name "))
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(" My topic "))
initialState.eventSink(RoomDetailsEditEvents.Save)
assertThat(room.newName).isNull()
assertThat(room.newTopic).isNull()
assertThat(room.newAvatarData).isNull()
assertThat(room.removedAvatar).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - save doesn't change topic if it was unset and is now blank`() = runTest {
val room = aMatrixRoom(topic = null, displayName = "Name", avatarUrl = AN_AVATAR_URL)
val room = aMatrixRoom(
topic = null,
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
canSendStateResult = { _, _ -> Result.success(true) }
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -441,17 +480,18 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(""))
initialState.eventSink(RoomDetailsEditEvents.Save)
assertThat(room.newName).isNull()
assertThat(room.newTopic).isNull()
assertThat(room.newAvatarData).isNull()
assertThat(room.removedAvatar).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - save doesn't change name if it's now empty`() = runTest {
val room = aMatrixRoom(topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL)
val room = aMatrixRoom(
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
canSendStateResult = { _, _ -> Result.success(true) }
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -459,17 +499,20 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName(""))
initialState.eventSink(RoomDetailsEditEvents.Save)
assertThat(room.newName).isNull()
assertThat(room.newTopic).isNull()
assertThat(room.newAvatarData).isNull()
assertThat(room.removedAvatar).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - save processes and sets avatar when processor returns successfully`() = runTest {
val room = aMatrixRoom(topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL)
val updateAvatarResult = lambdaRecorder { _: String, _: ByteArray -> Result.success(Unit) }
val room = aMatrixRoom(
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
updateAvatarResult = updateAvatarResult,
canSendStateResult = { _, _ -> Result.success(true) }
)
givenPickerReturnsFile()
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
@ -478,17 +521,19 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(RoomDetailsEditEvents.Save)
skipItems(3)
assertThat(room.newName).isNull()
assertThat(room.newTopic).isNull()
assertThat(room.newAvatarData).isSameInstanceAs(fakeFileContents)
assertThat(room.removedAvatar).isFalse()
skipItems(4)
updateAvatarResult.assertions().isCalledOnce().with(value("image/jpeg"), value(fakeFileContents))
}
}
@Test
fun `present - save does not set avatar data if processor fails`() = runTest {
val room = aMatrixRoom(topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL)
val room = aMatrixRoom(
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
canSendStateResult = { _, _ -> Result.success(true) }
)
fakePickerProvider.givenResult(anotherAvatarUri)
fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no")))
val presenter = createRoomDetailsEditPresenter(room)
@ -498,11 +543,7 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(RoomDetailsEditEvents.Save)
skipItems(2)
assertThat(room.newName).isNull()
assertThat(room.newTopic).isNull()
assertThat(room.newAvatarData).isNull()
assertThat(room.removedAvatar).isFalse()
skipItems(3)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
}
}
@ -514,9 +555,9 @@ class RoomDetailsEditPresenterTest {
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
).apply {
givenSetNameResult(Result.failure(Throwable("!")))
}
setNameResult = { Result.failure(Throwable("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name"))
}
@ -527,9 +568,9 @@ class RoomDetailsEditPresenterTest {
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
).apply {
givenSetTopicResult(Result.failure(Throwable("!")))
}
setTopicResult = { Result.failure(Throwable("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic"))
}
@ -540,9 +581,9 @@ class RoomDetailsEditPresenterTest {
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
).apply {
givenRemoveAvatarResult(Result.failure(Throwable("!")))
}
removeAvatarResult = { Result.failure(Throwable("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
}
@ -554,18 +595,22 @@ class RoomDetailsEditPresenterTest {
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
emitRoomInfo = true,
).apply {
givenUpdateAvatarResult(Result.failure(Throwable("!")))
}
updateAvatarResult = { _, _ -> Result.failure(Throwable("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
}
@Test
fun `present - CancelSaveChanges resets save action state`() = runTest {
givenPickerReturnsFile()
val room = aMatrixRoom(topic = "My topic", displayName = "Name", avatarUrl = AN_AVATAR_URL).apply {
givenSetTopicResult(Result.failure(Throwable("!")))
}
val room = aMatrixRoom(
topic = "My topic",
displayName = "Name",
avatarUrl = AN_AVATAR_URL,
setTopicResult = { Result.failure(Throwable("!")) },
canSendStateResult = { _, _ -> Result.success(true) }
)
val presenter = createRoomDetailsEditPresenter(room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -573,7 +618,7 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("foo"))
initialState.eventSink(RoomDetailsEditEvents.Save)
skipItems(2)
skipItems(3)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
initialState.eventSink(RoomDetailsEditEvents.CancelSaveChanges)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java)

View file

@ -52,7 +52,10 @@ class RoomMemberListPresenterTest {
@Test
fun `member loading is done automatically on start, but is async`() = runTest {
val room = FakeMatrixRoom().apply {
val room = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) },
canInviteResult = { Result.success(true) }
).apply {
// Needed to avoid discarding the loaded members as a partial and invalid result
givenRoomInfo(aRoomInfo(joinedMembersCount = 2))
}
@ -78,7 +81,12 @@ class RoomMemberListPresenterTest {
@Test
fun `open search`() = runTest {
val presenter = createPresenter()
val presenter = createPresenter(
matrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) },
canInviteResult = { Result.success(true) }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -93,7 +101,12 @@ class RoomMemberListPresenterTest {
@Test
fun `search for something which is not found`() = runTest {
val presenter = createPresenter()
val presenter = createPresenter(
matrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) },
canInviteResult = { Result.success(true) }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -112,7 +125,12 @@ class RoomMemberListPresenterTest {
@Test
fun `search for something which is found`() = runTest {
val presenter = createPresenter()
val presenter = createPresenter(
matrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) },
canInviteResult = { Result.success(true) }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -134,9 +152,10 @@ class RoomMemberListPresenterTest {
@Test
fun `present - asynchronously sets canInvite when user has correct power level`() = runTest {
val presenter = createPresenter(
matrixRoom = FakeMatrixRoom().apply {
givenCanInviteResult(Result.success(true))
}
matrixRoom = FakeMatrixRoom(
canInviteResult = { Result.success(true) },
updateMembersResult = { Result.success(Unit) }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -150,9 +169,10 @@ class RoomMemberListPresenterTest {
@Test
fun `present - asynchronously sets canInvite when user does not have correct power level`() = runTest {
val presenter = createPresenter(
matrixRoom = FakeMatrixRoom().apply {
givenCanInviteResult(Result.success(false))
}
matrixRoom = FakeMatrixRoom(
canInviteResult = { Result.success(false) },
updateMembersResult = { Result.success(Unit) }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -166,9 +186,10 @@ class RoomMemberListPresenterTest {
@Test
fun `present - asynchronously sets canInvite when power level check fails`() = runTest {
val presenter = createPresenter(
matrixRoom = FakeMatrixRoom().apply {
givenCanInviteResult(Result.failure(Throwable("Eek")))
}
matrixRoom = FakeMatrixRoom(
canInviteResult = { Result.failure(Throwable("Eek")) },
updateMembersResult = { Result.success(Unit) }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -183,7 +204,14 @@ class RoomMemberListPresenterTest {
fun `present - RoomMemberSelected by default opens the room member details through the navigator`() = runTest {
val navigator = FakeRoomMemberListNavigator()
val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = false)
val presenter = createPresenter(moderationPresenter = moderationPresenter, navigator = navigator)
val presenter = createPresenter(
moderationPresenter = moderationPresenter,
navigator = navigator,
matrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) },
canInviteResult = { Result.success(true) }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -205,7 +233,14 @@ class RoomMemberListPresenterTest {
val moderationPresenter = FakeRoomMembersModerationPresenter(canDisplayModerationActions = true).apply {
givenState(capturingState)
}
val presenter = createPresenter(moderationPresenter = moderationPresenter, navigator = navigator)
val presenter = createPresenter(
moderationPresenter = moderationPresenter,
navigator = navigator,
matrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) },
canInviteResult = { Result.success(true) }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -236,10 +271,12 @@ private fun TestScope.createDataSource(
@ExperimentalCoroutinesApi
private fun TestScope.createPresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixRoom: MatrixRoom = FakeMatrixRoom(),
matrixRoom: MatrixRoom = FakeMatrixRoom(
updateMembersResult = { Result.success(Unit) }
),
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers),
moderationPresenter: FakeRoomMembersModerationPresenter = FakeRoomMembersModerationPresenter(),
navigator: RoomMemberListNavigator = object : RoomMemberListNavigator { }
navigator: RoomMemberListNavigator = object : RoomMemberListNavigator {}
) = RoomMemberListPresenter(
room = matrixRoom,
roomMemberListDataSource = roomMemberListDataSource,

View file

@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
@ -53,9 +54,11 @@ class RoomMemberDetailsPresenterTest {
@Test
fun `present - returns the room member's data, then updates it if needed`() = runTest {
val roomMember = aRoomMember(displayName = "Alice")
val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.success("A custom name"))
givenUserAvatarUrlResult(Result.success("A custom avatar"))
val room = aMatrixRoom(
userDisplayNameResult = { Result.success("A custom name") },
userAvatarUrlResult = { Result.success("A custom avatar") },
getUpdatedMemberResult = { Result.success(roomMember) },
).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember)))
}
val presenter = createRoomMemberDetailsPresenter(
@ -82,11 +85,14 @@ class RoomMemberDetailsPresenterTest {
@Test
fun `present - will recover when retrieving room member details fails`() = runTest {
val roomMember = aRoomMember(displayName = "Alice")
val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.failure(Throwable()))
givenUserAvatarUrlResult(Result.failure(Throwable()))
val room = aMatrixRoom(
userDisplayNameResult = { Result.failure(Throwable()) },
userAvatarUrlResult = { Result.failure(Throwable()) },
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember)))
}
val presenter = createRoomMemberDetailsPresenter(
room = room,
roomMemberId = roomMember.userId
@ -105,9 +111,11 @@ class RoomMemberDetailsPresenterTest {
@Test
fun `present - will fallback to original data if the updated data is null`() = runTest {
val roomMember = aRoomMember(displayName = "Alice")
val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.success(null))
givenUserAvatarUrlResult(Result.success(null))
val room = aMatrixRoom(
userDisplayNameResult = { Result.success(null) },
userAvatarUrlResult = { Result.success(null) },
getUpdatedMemberResult = { Result.success(roomMember) }
).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember)))
}
val presenter = createRoomMemberDetailsPresenter(
@ -128,10 +136,11 @@ class RoomMemberDetailsPresenterTest {
@Test
fun `present - will fallback to user profile if user is not a member of the room`() = runTest {
val bobProfile = aMatrixUser("@bob:server.org", "Bob", avatarUrl = "anAvatarUrl")
val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.failure(Exception("Not a member!")))
givenUserAvatarUrlResult(Result.failure(Exception("Not a member!")))
}
val room = aMatrixRoom(
userDisplayNameResult = { Result.failure(Exception("Not a member!")) },
userAvatarUrlResult = { Result.failure(Exception("Not a member!")) },
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
)
val client = FakeMatrixClient().apply {
givenGetProfileResult(bobProfile.userId, Result.success(bobProfile))
}
@ -154,7 +163,13 @@ class RoomMemberDetailsPresenterTest {
@Test
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
val presenter = createRoomMemberDetailsPresenter()
val presenter = createRoomMemberDetailsPresenter(
room = aMatrixRoom(
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
userDisplayNameResult = { Result.success("Alice") },
userAvatarUrlResult = { Result.success("anAvatarUrl") },
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -176,6 +191,11 @@ class RoomMemberDetailsPresenterTest {
val client = FakeMatrixClient()
val roomMember = aRoomMember()
val presenter = createRoomMemberDetailsPresenter(
room = aMatrixRoom(
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
userDisplayNameResult = { Result.success("Alice") },
userAvatarUrlResult = { Result.success("anAvatarUrl") },
),
client = client,
roomMemberId = roomMember.userId
)
@ -199,13 +219,21 @@ class RoomMemberDetailsPresenterTest {
fun `present - BlockUser with error`() = runTest {
val matrixClient = FakeMatrixClient()
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE))
val presenter = createRoomMemberDetailsPresenter(client = matrixClient)
val presenter = createRoomMemberDetailsPresenter(
client = matrixClient,
room = aMatrixRoom(
getUpdatedMemberResult = { Result.success(aRoomMember(displayName = "Alice")) },
userDisplayNameResult = { Result.success("Alice") },
userAvatarUrlResult = { Result.success("anAvatarUrl") },
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
skipItems(2)
val errorState = awaitItem()
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
// Clear error
@ -218,13 +246,21 @@ class RoomMemberDetailsPresenterTest {
fun `present - UnblockUser with error`() = runTest {
val matrixClient = FakeMatrixClient()
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE))
val presenter = createRoomMemberDetailsPresenter(client = matrixClient)
val presenter = createRoomMemberDetailsPresenter(
room = aMatrixRoom(
getUpdatedMemberResult = { Result.success(aRoomMember(displayName = "Alice")) },
userDisplayNameResult = { Result.success("Alice") },
userAvatarUrlResult = { Result.success("anAvatarUrl") },
),
client = matrixClient,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
skipItems(2)
val errorState = awaitItem()
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
// Clear error
@ -235,7 +271,13 @@ class RoomMemberDetailsPresenterTest {
@Test
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
val presenter = createRoomMemberDetailsPresenter()
val presenter = createRoomMemberDetailsPresenter(
room = aMatrixRoom(
getUpdatedMemberResult = { Result.failure(AN_EXCEPTION) },
userDisplayNameResult = { Result.success("Alice") },
userAvatarUrlResult = { Result.success("anAvatarUrl") },
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -255,7 +297,14 @@ class RoomMemberDetailsPresenterTest {
@Test
fun `present - start DM action complete scenario`() = runTest {
val startDMAction = FakeStartDMAction()
val presenter = createRoomMemberDetailsPresenter(startDMAction = startDMAction)
val presenter = createRoomMemberDetailsPresenter(
room = aMatrixRoom(
getUpdatedMemberResult = { Result.success(aRoomMember(displayName = "Alice")) },
userDisplayNameResult = { Result.success("Alice") },
userAvatarUrlResult = { Result.success("anAvatarUrl") },
),
startDMAction = startDMAction,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -268,6 +317,7 @@ class RoomMemberDetailsPresenterTest {
startDMAction.givenExecuteResult(startDMFailureResult)
initialState.eventSink(UserProfileEvents.StartDM)
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
skipItems(2)
awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(startDMFailureResult)
state.eventSink(UserProfileEvents.ClearStartDMState)
@ -292,8 +342,8 @@ class RoomMemberDetailsPresenterTest {
}
private fun createRoomMemberDetailsPresenter(
room: MatrixRoom,
client: MatrixClient = FakeMatrixClient(),
room: MatrixRoom = aMatrixRoom(),
roomMemberId: UserId = UserId("@alice:server.org"),
startDMAction: StartDMAction = FakeStartDMAction()
): RoomMemberDetailsPresenter {

View file

@ -54,29 +54,34 @@ class DefaultRoomMembersModerationPresenterTest {
@Test
fun `canDisplayModerationActions - when user can kick other users, FF is enabled and room is not a DM returns true`() = runTest {
val room = FakeMatrixRoom(isDirect = false, activeMemberCount = 10).apply {
givenCanKickResult(Result.success(true))
}
val room = FakeMatrixRoom(
isDirect = false,
activeMemberCount = 10,
canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) },
)
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
assertThat(presenter.canDisplayModerationActions()).isTrue()
}
@Test
fun `canDisplayModerationActions - when user can ban other users, FF is enabled and room is not a DM returns true`() = runTest {
val room = FakeMatrixRoom(isDirect = false, activeMemberCount = 10).apply {
givenCanBanResult(Result.success(true))
}
val room = FakeMatrixRoom(
isDirect = false,
activeMemberCount = 10,
canBanResult = { Result.success(true) },
)
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
assertThat(presenter.canDisplayModerationActions()).isTrue()
}
@Test
fun `present - SelectRoomMember when the current user has permissions displays member actions`() = runTest {
val room = FakeMatrixRoom().apply {
givenCanKickResult(Result.success(true))
givenCanBanResult(Result.success(true))
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
}
val room = FakeMatrixRoom(
canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
)
val selectedMember = aVictor()
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
@ -98,11 +103,12 @@ class DefaultRoomMembersModerationPresenterTest {
@Test
fun `present - SelectRoomMember displays only view profile if selected member has same power level as the current user`() = runTest {
val room = FakeMatrixRoom(sessionId = A_USER_ID).apply {
givenCanKickResult(Result.success(true))
givenCanBanResult(Result.success(true))
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
}
val room = FakeMatrixRoom(
sessionId = A_USER_ID,
canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
)
val selectedMember = aRoomMember(A_USER_ID_2, powerLevel = 100L)
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
@ -123,11 +129,11 @@ class DefaultRoomMembersModerationPresenterTest {
@Test
fun `present - SelectRoomMember displays an unban confirmation dialog when the member is banned`() = runTest {
val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN)
val room = FakeMatrixRoom().apply {
givenCanKickResult(Result.success(true))
givenCanBanResult(Result.success(true))
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
}
val room = FakeMatrixRoom(
canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
)
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -144,11 +150,12 @@ class DefaultRoomMembersModerationPresenterTest {
@Test
fun `present - Kick removes the user`() = runTest {
val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom().apply {
givenCanKickResult(Result.success(true))
givenCanBanResult(Result.success(true))
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
}
val room = FakeMatrixRoom(
canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
kickUserResult = { _, _ -> Result.success(Unit) },
)
val selectedMember = aVictor()
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) {
@ -171,11 +178,12 @@ class DefaultRoomMembersModerationPresenterTest {
@Test
fun `present - BanUser requires confirmation and then bans the user`() = runTest {
val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom().apply {
givenCanKickResult(Result.success(true))
givenCanBanResult(Result.success(true))
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
}
val room = FakeMatrixRoom(
canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
banUserResult = { _, _ -> Result.success(Unit) },
)
val selectedMember = aVictor()
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) {
@ -204,11 +212,13 @@ class DefaultRoomMembersModerationPresenterTest {
fun `present - UnbanUser requires confirmation and then unbans the user`() = runTest {
val analyticsService = FakeAnalyticsService()
val selectedMember = aRoomMember(A_USER_ID_2, membership = RoomMembershipState.BAN)
val room = FakeMatrixRoom().apply {
givenCanKickResult(Result.success(true))
givenCanBanResult(Result.success(true))
val room = FakeMatrixRoom(
canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.ADMIN) },
unBanUserResult = { _, _ -> Result.success(Unit) },
).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(selectedMember)))
givenUserRoleResult(Result.success(RoomMember.Role.ADMIN))
}
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room, analyticsService = analyticsService)
moleculeFlow(RecompositionMode.Immediate) {
@ -231,10 +241,11 @@ class DefaultRoomMembersModerationPresenterTest {
@Test
fun `present - Reset removes the selected user and actions`() = runTest {
val room = FakeMatrixRoom().apply {
givenCanKickResult(Result.success(true))
givenCanBanResult(Result.success(true))
}
val room = FakeMatrixRoom(
canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) },
userRoleResult = { Result.success(RoomMember.Role.USER) },
)
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -251,13 +262,14 @@ class DefaultRoomMembersModerationPresenterTest {
@Test
fun `present - Reset resets any async actions`() = runTest {
val room = FakeMatrixRoom().apply {
givenCanKickResult(Result.success(true))
givenCanBanResult(Result.success(true))
givenKickUserResult(Result.failure(Throwable("Eek")))
givenBanUserResult(Result.failure(Throwable("Eek")))
givenUnbanUserResult(Result.failure(Throwable("Eek")))
}
val room = FakeMatrixRoom(
canKickResult = { Result.success(true) },
canBanResult = { Result.success(true) },
kickUserResult = { _, _ -> Result.failure(Throwable("Eek")) },
banUserResult = { _, _ -> Result.failure(Throwable("Eek")) },
unBanUserResult = { _, _ -> Result.failure(Throwable("Eek")) },
userRoleResult = { Result.success(RoomMember.Role.USER) },
)
val presenter = createDefaultRoomMembersModerationPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.defaultRoomPowerLevels
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -67,7 +68,12 @@ class RolesAndPermissionPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - DemoteSelfTo changes own role to the specified one`() = runTest(StandardTestDispatcher()) {
val presenter = createRolesAndPermissionsPresenter(dispatchers = testCoroutineDispatchers())
val presenter = createRolesAndPermissionsPresenter(
dispatchers = testCoroutineDispatchers(),
room = FakeMatrixRoom(
updateUserRoleResult = { Result.success(Unit) }
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -85,9 +91,9 @@ class RolesAndPermissionPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - DemoteSelfTo can handle failures and clean them`() = runTest(StandardTestDispatcher()) {
val room = FakeMatrixRoom().apply {
givenUpdateUserRoleResult(Result.failure(Exception("Failed to update role")))
}
val room = FakeMatrixRoom(
updateUserRoleResult = { Result.failure(Exception("Failed to update role")) }
)
val presenter = createRolesAndPermissionsPresenter(room = room, dispatchers = testCoroutineDispatchers())
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -123,7 +129,12 @@ class RolesAndPermissionPresenterTest {
@Test
fun `present - ResetPermissions needs confirmation, then resets permissions`() = runTest {
val analyticsService = FakeAnalyticsService()
val presenter = createRolesAndPermissionsPresenter(analyticsService = analyticsService)
val presenter = createRolesAndPermissionsPresenter(
analyticsService = analyticsService,
room = FakeMatrixRoom(
resetPowerLevelsResult = { Result.success(defaultRoomPowerLevels()) }
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {

View file

@ -275,7 +275,10 @@ class ChangeRolesPresenterTest {
@Test
fun `present - Save will display a confirmation when adding admins`() = runTest {
val room = FakeMatrixRoom().apply {
val room = FakeMatrixRoom(
updateUserRoleResult = { Result.success(Unit) },
updateMembersResult = { Result.success(Unit) },
).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 100)))
}
@ -325,7 +328,10 @@ class ChangeRolesPresenterTest {
@Test
fun `present - Save will just save the data for moderators`() = runTest {
val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom().apply {
val room = FakeMatrixRoom(
updateUserRoleResult = { Result.success(Unit) },
updateMembersResult = { Result.success(Unit) },
).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 50)))
}
@ -351,10 +357,11 @@ class ChangeRolesPresenterTest {
@Test
fun `present - Save can handle failures and ClearError clears them`() = runTest {
val room = FakeMatrixRoom().apply {
val room = FakeMatrixRoom(
updateUserRoleResult = { Result.failure(IllegalStateException("Failed")) }
).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
givenRoomInfo(aRoomInfo(userPowerLevels = persistentMapOf(A_USER_ID to 50)))
givenUpdateUserRoleResult(Result.failure(IllegalStateException("Failed")))
}
val presenter = createChangeRolesPresenter(role = RoomMember.Role.MODERATOR, room = room)
moleculeFlow(RecompositionMode.Immediate) {

View file

@ -164,7 +164,13 @@ class ChangeRoomPermissionsPresenterTest {
@Test
fun `present - Save updates the current permissions and resets hasChanges`() = runTest {
val analyticsService = FakeAnalyticsService()
val presenter = createChangeRoomPermissionsPresenter(analyticsService = analyticsService)
val presenter = createChangeRoomPermissionsPresenter(
analyticsService = analyticsService,
room = FakeMatrixRoom(
updatePowerLevelsResult = { Result.success(Unit) },
powerLevelsResult = { Result.success(defaultPermissions()) }
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -208,9 +214,9 @@ class ChangeRoomPermissionsPresenterTest {
@Test
fun `present - Save will fail if there are not current permissions`() = runTest {
val room = FakeMatrixRoom().apply {
givenPowerLevelsResult(Result.failure(IllegalStateException("Failed to load power levels")))
}
val room = FakeMatrixRoom(
powerLevelsResult = { Result.failure(IllegalStateException("Failed to load power levels")) }
)
val presenter = createChangeRoomPermissionsPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -225,9 +231,10 @@ class ChangeRoomPermissionsPresenterTest {
@Test
fun `present - Save can handle failures and they can be cleared`() = runTest {
val room = FakeMatrixRoom().apply {
givenUpdatePowerLevelsResult(Result.failure(IllegalStateException("Failed to update power levels")))
}
val room = FakeMatrixRoom(
powerLevelsResult = { Result.success(defaultPermissions()) },
updatePowerLevelsResult = { Result.failure(IllegalStateException("Failed to update power levels")) },
)
val presenter = createChangeRoomPermissionsPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -292,7 +299,9 @@ class ChangeRoomPermissionsPresenterTest {
private fun createChangeRoomPermissionsPresenter(
section: ChangeRoomPermissionsSection = ChangeRoomPermissionsSection.RoomDetails,
room: FakeMatrixRoom = FakeMatrixRoom(),
room: FakeMatrixRoom = FakeMatrixRoom(
powerLevelsResult = { Result.success(defaultPermissions()) }
),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
) = ChangeRoomPermissionsPresenter(
section = section,

View file

@ -25,12 +25,13 @@
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Μπορείς να καταργήσεις την επιλογή φίλτρων για να δεις τις άλλες συνομιλίες σου"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Δεν έχεις συνομιλίες για αυτήν την επιλογή"</string>
<string name="screen_roomlist_filter_people">"Άτομα"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Δεν έχεις ακόμα ΠΜ"</string>
<string name="screen_roomlist_filter_rooms">"Δωμάτια"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Δεν είσαι ακόμα σε κανένα δωμάτιο"</string>
<string name="screen_roomlist_filter_unreads">"Μη αναγνωσμένα"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Συγχαρητήρια!
Δεν έχεις μη αναγνωσμένα μηνύματα!"</string>
<string name="screen_roomlist_main_space_title">"Συζητήσεις"</string>
<string name="screen_roomlist_main_space_title">"Συνομιλίες"</string>
<string name="screen_roomlist_mark_as_read">"Επισήμανση ως αναγνωσμένου"</string>
<string name="screen_roomlist_mark_as_unread">"Επισήμανση ως μη αναγνωσμένου"</string>
<string name="screen_roomlist_room_directory_button_title">"Περιήγηση σε όλα τα δωμάτια"</string>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"თქვენი ჩეთების სარეზერვო ასლი ამჟამად არ არის სინქრონიზებული. თქვენ უნდა შეიყვანოთ თქვენი აღდგენის გასაღები, რათა შეინარჩუნოთ წვდომა ჩეთების სარეზერვო ასლზე."</string>
<string name="confirm_recovery_key_banner_title">"შეიყვანეთ აღდგენის გასაღები"</string>
<string name="screen_invites_decline_chat_message">"დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ მოწვევაზე %1$s-ში?"</string>
<string name="screen_invites_decline_chat_title">"მოწვევაზე უარის თქმა"</string>
<string name="screen_invites_decline_direct_chat_message">"დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ ჩატზე %1$s-თან?"</string>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Twoja kopia zapasowa czatu jest obecnie niezsynchronizowana. Aby zachować dostęp do kopii zapasowej czatu, musisz potwierdzić klucz odzyskiwania."</string>
<string name="confirm_recovery_key_banner_title">"Potwierdź klucz odzyskiwania"</string>
<string name="full_screen_intent_banner_message">"Upewnij się, że nie pominiesz żadnego połączenia. Zmień swoje ustawienia i zezwól na powiadomienia na blokadzie ekranu."</string>
<string name="full_screen_intent_banner_title">"Popraw jakość swoich rozmów"</string>
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie do dołączenia do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Odrzuć czat"</string>
<string name="screen_invites_empty_list">"Brak zaproszeń"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) zaprosił Cię"</string>
<string name="screen_migration_message">"Jest to jednorazowy proces, dziękujemy za czekanie."</string>
<string name="screen_migration_title">"Konfigurowanie Twojego konta."</string>
<string name="screen_roomlist_a11y_create_message">"Utwórz nową rozmowę lub pokój"</string>
<string name="screen_roomlist_empty_message">"Wyślij komuś wiadomość, aby rozpocząć."</string>
<string name="screen_roomlist_empty_title">"Brak czatów."</string>
<string name="screen_roomlist_filter_people">"Osoby"</string>
<string name="screen_roomlist_main_space_title">"Wszystkie czaty"</string>
<string name="session_verification_banner_message">"Wygląda na to, że używasz nowego urządzenia. Zweryfikuj się innym urządzeniem, aby uzyskać dostęp do zaszyfrowanych wiadomości."</string>
<string name="session_verification_banner_title">"Potwierdź, że to Ty"</string>
</resources>

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