Merge branch 'develop' of github.com:element-hq/element-x-android into align-cta-button-on-login-flow

# Conflicts:
#	features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_0,NEXUS_5,1.0,en].png
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_1,NEXUS_5,1.0,en].png
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_2,NEXUS_5,1.0,en].png
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_3,NEXUS_5,1.0,en].png
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_4,NEXUS_5,1.0,en].png
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_0,NEXUS_5,1.0,en].png
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_1,NEXUS_5,1.0,en].png
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_2,NEXUS_5,1.0,en].png
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_3,NEXUS_5,1.0,en].png
#	tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_4,NEXUS_5,1.0,en].png
This commit is contained in:
Marco Antonio Alvarez 2024-02-01 17:54:11 +01:00
commit f98cd5b99b
694 changed files with 6806 additions and 1630 deletions

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_share_data">"Condividi dati statistici"</string>
<string name="screen_analytics_settings_help_us_improve">"Condividi dati di utilizzo anonimi per aiutarci a identificare problemi."</string>
<string name="screen_analytics_settings_read_terms">"Puoi leggere tutti i nostri termini %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"qui"</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">"Non registreremo né profileremo alcun dato personale"</string>
<string name="screen_analytics_prompt_help_us_improve">"Condividi dati di utilizzo anonimi per aiutarci a identificare problemi."</string>
<string name="screen_analytics_prompt_read_terms">"Puoi leggere tutti i nostri termini %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"qui"</string>
<string name="screen_analytics_prompt_settings">"Puoi disattivarlo in qualsiasi momento"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Non condivideremo i tuoi dati con terze parti"</string>
<string name="screen_analytics_prompt_title">"Aiutaci a migliorare %1$s"</string>
</resources>

View file

@ -45,7 +45,7 @@ import io.element.android.features.call.CallForegroundService
import io.element.android.features.call.CallType
import io.element.android.features.call.di.CallBindings
import io.element.android.features.call.utils.CallIntentDataParser
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.architecture.bindings
import javax.inject.Inject
@ -67,7 +67,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
@Inject lateinit var callIntentDataParser: CallIntentDataParser
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
@Inject lateinit var preferencesStore: PreferencesStore
@Inject lateinit var appPreferencesStore: AppPreferencesStore
private lateinit var presenter: CallScreenPresenter
@ -101,7 +101,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
setContent {
val theme by remember {
preferencesStore.getThemeFlow().mapToTheme()
appPreferencesStore.getThemeFlow().mapToTheme()
}
.collectAsState(initial = Theme.System)
val state = presenter.present()

View file

@ -18,7 +18,7 @@ package io.element.android.features.call.utils
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.RoomId
@ -31,7 +31,7 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultCallWidgetProvider @Inject constructor(
private val matrixClientsProvider: MatrixClientProvider,
private val preferencesStore: PreferencesStore,
private val appPreferencesStore: AppPreferencesStore,
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
) : CallWidgetProvider {
override suspend fun getWidget(
@ -42,8 +42,8 @@ class DefaultCallWidgetProvider @Inject constructor(
theme: String?,
): Result<Pair<MatrixWidgetDriver, String>> = runCatching {
val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found")
val baseUrl = preferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl)
val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = room.isEncrypted)
val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow()
room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl
}

View file

@ -1,6 +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">"Folyamatban lévő hívás"</string>
<string name="call_foreground_service_message_android">"Koppintson a híváshoz való visszatéréshez"</string>
<string name="call_foreground_service_message_android">"Koppints a híváshoz való visszatéréshez"</string>
<string name="call_foreground_service_title_android">"☎️ Hívás folyamatban"</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">"Chiamata in corso"</string>
<string name="call_foreground_service_message_android">"Tocca per tornare alla chiamata"</string>
<string name="call_foreground_service_title_android">"☎️ Chiamata in corso"</string>
</resources>

View file

@ -2,5 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Текущий вызов"</string>
<string name="call_foreground_service_message_android">"Коснитесь, чтобы вернуться к вызову"</string>
<string name="call_foreground_service_title_android">"Идёт вызов"</string>
<string name="call_foreground_service_title_android">"☎️ Идёт вызов"</string>
</resources>

View file

@ -17,8 +17,8 @@
package io.element.android.features.call.utils
import com.google.common.truth.Truth.assertThat
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -94,14 +94,14 @@ class DefaultCallWidgetProviderTest {
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val preferencesStore = InMemoryPreferencesStore().apply {
val preferencesStore = InMemoryAppPreferencesStore().apply {
setCustomElementCallBaseUrl("https://custom.element.io")
}
val settingsProvider = FakeCallWidgetSettingsProvider()
val provider = createProvider(
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
callWidgetSettingsProvider = settingsProvider,
preferencesStore = preferencesStore,
appPreferencesStore = preferencesStore,
)
provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme")
@ -110,11 +110,11 @@ class DefaultCallWidgetProviderTest {
private fun createProvider(
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
preferencesStore: PreferencesStore = InMemoryPreferencesStore(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider()
) = DefaultCallWidgetProvider(
matrixClientProvider,
preferencesStore,
appPreferencesStore,
callWidgetSettingsProvider,
)
}

View file

@ -23,10 +23,11 @@ import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.usersearch.api.UserSearchResult
@Composable
@ -36,23 +37,24 @@ fun SearchMultipleUsersResultItem(
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
if (searchResult.isUnresolved) {
CheckableUnresolvedUserRow(
checked = isUserSelected,
modifier = modifier,
val data = if (searchResult.isUnresolved) {
CheckableUserRowData.Unresolved(
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
id = searchResult.matrixUser.userId.value,
onCheckedChange = onCheckedChange,
)
} else {
CheckableMatrixUserRow(
checked = isUserSelected,
modifier = modifier,
matrixUser = searchResult.matrixUser,
avatarSize = AvatarSize.UserListItem,
onCheckedChange = onCheckedChange,
CheckableUserRowData.Resolved(
name = searchResult.matrixUser.getBestName(),
subtext = if (searchResult.matrixUser.displayName.isNullOrEmpty()) null else searchResult.matrixUser.userId.value,
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
)
}
CheckableUserRow(
checked = isUserSelected,
modifier = modifier,
data = data,
onCheckedChange = onCheckedChange,
)
}
@Preview

View file

@ -1,8 +1,15 @@
<?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">"Nuova stanza"</string>
<string name="screen_create_room_action_invite_people">"Invita persone"</string>
<string name="screen_create_room_add_people_title">"Aggiungi persone"</string>
<string name="screen_create_room_action_invite_people">"Invita persone su Element"</string>
<string name="screen_create_room_add_people_title">"Invita persone"</string>
<string name="screen_create_room_error_creating_room">"Si è verificato un errore durante la creazione della stanza"</string>
<string name="screen_create_room_private_option_description">"I messaggi in questa stanza sono cifrati. La crittografia non può essere disattivata in seguito."</string>
<string name="screen_create_room_private_option_title">"Stanza privata (solo su invito)"</string>
<string name="screen_create_room_public_option_description">"I messaggi non sono cifrati e chiunque può leggerli. Puoi attivare la crittografia in un secondo momento."</string>
<string name="screen_create_room_public_option_title">"Stanza pubblica (chiunque)"</string>
<string name="screen_create_room_room_name_label">"Nome stanza"</string>
<string name="screen_create_room_topic_label">"Argomento (facoltativo)"</string>
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
<string name="screen_create_room_title">"Crea una stanza"</string>
</resources>

View file

@ -4,9 +4,9 @@
<string name="screen_create_room_action_invite_people">"Пригласите друзей в Element"</string>
<string name="screen_create_room_add_people_title">"Пригласить людей"</string>
<string name="screen_create_room_error_creating_room">"Произошла ошибка при создании комнаты"</string>
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате зашифрованы. Отключить шифрование впоследствии невозможно."</string>
<string name="screen_create_room_private_option_description">"Сообщения в этой комнате зашифрованы. Отключить шифрование позже будет невозможно."</string>
<string name="screen_create_room_private_option_title">"Приватная комната (только по приглашению)"</string>
<string name="screen_create_room_public_option_description">"Сообщения не зашифрованы, и каждый может их прочитать. Вы можете включить шифрование позже."</string>
<string name="screen_create_room_public_option_description">"Сообщения не зашифрованы, каждый может их прочитать. Вы можете включить шифрование позже."</string>
<string name="screen_create_room_public_option_title">"Публичная комната (любой)"</string>
<string name="screen_create_room_room_name_label">"Название комнаты"</string>
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>

View file

@ -18,6 +18,8 @@ package io.element.android.features.ftue.impl.migration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.ftue.impl.R
@ -32,8 +34,9 @@ fun MigrationScreenView(
modifier: Modifier = Modifier,
) {
if (migrationState.isMigrating.not()) {
val latestOnMigrationFinished by rememberUpdatedState(onMigrationFinished)
LaunchedEffect(Unit) {
onMigrationFinished()
latestOnMigrationFinished()
}
}
SunsetPage(

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
<string name="screen_migration_title">"Richte dein Konto ein."</string>
<string name="screen_migration_title">"Dein Konto wird eingerichtet."</string>
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Si tratta di una procedura che si effettua una sola volta, grazie per l\'attesa."</string>
<string name="screen_migration_title">"Configurazione del tuo account."</string>
<string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
<string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string>
<string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string>
<string name="screen_welcome_bullet_3">"Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni."</string>
<string name="screen_welcome_button">"Andiamo!"</string>
<string name="screen_welcome_subtitle">"Ecco cosa c\'è da sapere:"</string>
<string name="screen_welcome_title">"Benvenuti in %1$s!"</string>
</resources>

View file

@ -169,7 +169,7 @@ class InviteListPresenter @Inject constructor(
AvatarData(
id = roomId.value,
name = name,
url = avatarURLString,
url = avatarUrl,
size = AvatarSize.RoomInviteItem,
)
}

View file

@ -28,6 +28,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -57,8 +59,9 @@ fun InviteListView(
modifier: Modifier = Modifier,
) {
if (state.acceptedAction is AsyncData.Success) {
val latestOnInviteAccepted by rememberUpdatedState(onInviteAccepted)
LaunchedEffect(state.acceptedAction) {
onInviteAccepted(state.acceptedAction.data)
latestOnInviteAccepted(state.acceptedAction.data)
}
}

View file

@ -1,4 +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">"Vuoi davvero rifiutare l\'invito ad entrare in %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Rifiuta l\'invito"</string>
<string name="screen_invites_decline_direct_chat_message">"Vuoi davvero rifiutare questa chat privata con %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Rifiuta la chat"</string>
<string name="screen_invites_empty_list">"Nessun invito"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) ti ha invitato"</string>
</resources>

View file

@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Вы уверены, что хотите отклонить приглашение в %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Отклонить приглашение"</string>
<string name="screen_invites_decline_direct_chat_message">"Вы уверены, что хотите отказаться от приватного общения с %1$s?"</string>
<string name="screen_invites_decline_direct_chat_message">"Вы уверены, что хотите отказаться от личного общения с %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Отклонить чат"</string>
<string name="screen_invites_empty_list">"Нет приглашений"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) пригласил вас"</string>

View file

@ -30,7 +30,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
@ -39,6 +38,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
@ -425,14 +425,12 @@ class InviteListPresenterTests {
postInviteRooms(
listOf(
RoomSummary.Filled(
RoomSummaryDetails(
aRoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_ROOM_NAME,
avatarURLString = null,
avatarUrl = null,
isDirect = false,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
inviter = RoomMember(
userId = A_USER_ID,
displayName = A_USER_NAME,
@ -454,14 +452,12 @@ class InviteListPresenterTests {
postInviteRooms(
listOf(
RoomSummary.Filled(
RoomSummaryDetails(
aRoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_ROOM_NAME,
avatarURLString = null,
avatarUrl = null,
isDirect = true,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
inviter = RoomMember(
userId = A_USER_ID,
displayName = A_USER_NAME,
@ -480,14 +476,12 @@ class InviteListPresenterTests {
}
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
RoomSummaryDetails(
aRoomSummaryDetails(
roomId = id,
name = A_ROOM_NAME,
avatarURLString = null,
avatarUrl = null,
isDirect = false,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
)
)

View file

@ -26,6 +26,7 @@ data class LeaveRoomState(
) {
sealed interface Confirmation {
data object Hidden : Confirmation
data class Dm(val roomId: RoomId) : Confirmation
data class Generic(val roomId: RoomId) : Confirmation
data class PrivateRoom(val roomId: RoomId) : Confirmation
data class LastUserInRoom(val roomId: RoomId) : Confirmation

View file

@ -28,17 +28,17 @@ class LeaveRoomStateProvider : PreviewParameterProvider<LeaveRoomState> {
error = LeaveRoomState.Error.Hidden,
),
aLeaveRoomState(
confirmation = LeaveRoomState.Confirmation.Generic(A_ROOM_ID),
confirmation = LeaveRoomState.Confirmation.Generic(roomId = A_ROOM_ID),
progress = LeaveRoomState.Progress.Hidden,
error = LeaveRoomState.Error.Hidden,
),
aLeaveRoomState(
confirmation = LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID),
confirmation = LeaveRoomState.Confirmation.PrivateRoom(roomId = A_ROOM_ID),
progress = LeaveRoomState.Progress.Hidden,
error = LeaveRoomState.Error.Hidden,
),
aLeaveRoomState(
confirmation = LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID),
confirmation = LeaveRoomState.Confirmation.LastUserInRoom(roomId = A_ROOM_ID),
progress = LeaveRoomState.Progress.Hidden,
error = LeaveRoomState.Error.Hidden,
),
@ -52,6 +52,11 @@ class LeaveRoomStateProvider : PreviewParameterProvider<LeaveRoomState> {
progress = LeaveRoomState.Progress.Hidden,
error = LeaveRoomState.Error.Shown,
),
aLeaveRoomState(
confirmation = LeaveRoomState.Confirmation.Dm(roomId = A_ROOM_ID),
progress = LeaveRoomState.Progress.Hidden,
error = LeaveRoomState.Error.Hidden,
),
)
}

View file

@ -47,21 +47,32 @@ private fun LeaveRoomConfirmationDialog(
) {
when (state.confirmation) {
is LeaveRoomState.Confirmation.Hidden -> {}
is LeaveRoomState.Confirmation.Dm -> LeaveRoomConfirmationDialog(
text = R.string.leave_conversation_alert_subtitle,
roomId = state.confirmation.roomId,
isDm = true,
eventSink = state.eventSink,
)
is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
text = R.string.leave_room_alert_private_subtitle,
roomId = state.confirmation.roomId,
isDm = false,
eventSink = state.eventSink,
)
is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
text = R.string.leave_room_alert_empty_subtitle,
roomId = state.confirmation.roomId,
isDm = false,
eventSink = state.eventSink,
)
is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog(
text = R.string.leave_room_alert_subtitle,
roomId = state.confirmation.roomId,
isDm = false,
eventSink = state.eventSink,
)
}
@ -71,10 +82,11 @@ private fun LeaveRoomConfirmationDialog(
private fun LeaveRoomConfirmationDialog(
@StringRes text: Int,
roomId: RoomId,
isDm: Boolean,
eventSink: (LeaveRoomEvent) -> Unit,
) {
ConfirmationDialog(
title = stringResource(CommonStrings.action_leave_room),
title = stringResource(if (isDm) CommonStrings.action_leave_conversation else CommonStrings.action_leave_room),
content = stringResource(text),
submitText = stringResource(CommonStrings.action_leave),
onSubmitClicked = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) },

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Opravdu chcete opustit tuto konverzaci? Tato konverzace není veřejná a bez pozvánky se k ní nebudete moci znovu připojit."</string>
<string name="leave_room_alert_empty_subtitle">"Opravdu chcete opustit tuto místnost? Jste tu jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás."</string>
<string name="leave_room_alert_private_subtitle">"Opravdu chcete opustit tuto místnost? Tato místnost není veřejná a bez pozvánky se nebudete moci znovu připojit."</string>
<string name="leave_room_alert_subtitle">"Opravdu chcete opustit místnost?"</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Êtes-vous sûr de vouloir quitter cette discussion? Vous ne pourrez pas la rejoindre à nouveau sans y être invité."</string>
<string name="leave_room_alert_empty_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre le salon à lavenir, y compris vous."</string>
<string name="leave_room_alert_private_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Ce salon nest pas public et vous ne pourrez pas le rejoindre sans invitation."</string>
<string name="leave_room_alert_subtitle">"Êtes-vous sûr de vouloir quitter le salon ?"</string>

View file

@ -1,6 +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">"Biztos, hogy elhagyja ezt a szobát? Ön az egyedüli ember itt. Ha kilép, akkor senki sem fog tudni csatlakozni a jövőben, Önt is beleértve."</string>
<string name="leave_room_alert_private_subtitle">"Biztos, hogy elhagyja ezt a szobát? Ez a szoba nem nyilvános, és meghívó nélkül nem fog tudni újra belépni."</string>
<string name="leave_room_alert_subtitle">"Biztos, hogy elhagyja a szobát?"</string>
<string name="leave_room_alert_private_subtitle">"Biztos, hogy elhagyod ezt a szobát? Ez a szoba nem nyilvános, és meghívó nélkül nem fogsz tudni újra belépni."</string>
<string name="leave_room_alert_subtitle">"Biztos, hogy elhagyod a szobát?"</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Vuoi davvero abbandonare questa conversazione? La conversazione non è pubblica e non potrai rientrare senza un invito."</string>
<string name="leave_room_alert_empty_subtitle">"Sei sicuro di voler lasciare questa stanza? Sei l\'unica persona presente. Se esci, nessuno potrà unirsi in futuro, te compreso."</string>
<string name="leave_room_alert_private_subtitle">"Sei sicuro di voler lasciare questa stanza? Questa stanza non è pubblica e non potrai rientrare senza un invito."</string>
<string name="leave_room_alert_subtitle">"Sei sicuro di voler lasciare la stanza?"</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Вы уверены, что хотите покинуть беседу?"</string>
<string name="leave_room_alert_empty_subtitle">"Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."</string>
<string name="leave_room_alert_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."</string>
<string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Ste si istí, že chcete opustiť konverzáciu?"</string>
<string name="leave_room_alert_empty_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Ste tu jediná osoba. Ak odídete, nikto sa do nej nebude môcť v budúcnosti pripojiť, vrátane vás."</string>
<string name="leave_room_alert_private_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Táto miestnosť nie je verejná a bez pozvania sa do nej nebudete môcť vrátiť."</string>
<string name="leave_room_alert_subtitle">"Ste si istí, že chcete opustiť miestnosť?"</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Are you sure that you want to leave this conversation? This conversation is not public and you won\'t be able to rejoin without an invite."</string>
<string name="leave_room_alert_empty_subtitle">"Are you sure that you want to leave this room? You\'re the only person here. If you leave, no one will be able to join in the future, including you."</string>
<string name="leave_room_alert_private_subtitle">"Are you sure that you want to leave this room? This room is not public and you won\'t be able to rejoin without an invite."</string>
<string name="leave_room_alert_subtitle">"Are you sure that you want to leave the room?"</string>

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Dm
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Generic
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom
@ -85,6 +86,7 @@ private suspend fun showLeaveRoomAlert(
) {
matrixClient.getRoom(roomId)?.use { room ->
confirmation.value = when {
room.isDm -> Dm(roomId)
!room.isPublic -> PrivateRoom(roomId)
room.joinedMemberCount == 1L -> LastUserInRoom(roomId)
else -> Generic(roomId)

View file

@ -114,6 +114,26 @@ class LeaveRoomPresenterImplTest {
}
}
@Test
fun `present - show DM confirmation`() = runTest {
val presenter = createLeaveRoomPresenter(
client = FakeMatrixClient().apply {
givenGetRoomResult(
roomId = A_ROOM_ID,
result = FakeMatrixRoom(activeMemberCount = 2, isDirect = true, isOneToOne = true),
)
}
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
val confirmationState = awaitItem()
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Dm(A_ROOM_ID))
}
}
@Test
fun `present - leaving a room leaves the room`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()

View file

@ -18,6 +18,8 @@ package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
@ -30,15 +32,16 @@ class PinUnlockHelper @Inject constructor(
) {
@Composable
fun OnUnlockEffect(onUnlock: () -> Unit) {
val latestOnUnlock by rememberUpdatedState(onUnlock)
DisposableEffect(Unit) {
val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
override fun onBiometricUnlockSuccess() {
onUnlock()
latestOnUnlock()
}
}
val pinCodeVerifiedCallback = object : DefaultPinCodeManagerCallback() {
override fun onPinCodeVerified() {
onUnlock()
latestOnUnlock()
}
}
biometricUnlockManager.addCallback(biometricUnlockCallback)

View file

@ -1,4 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Hai %1$d tentativo di sblocco"</item>
<item quantity="other">"Hai %1$d tentativi di sblocco"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"PIN sbagliato. Hai %1$d altro tentativo"</item>
<item quantity="other">"PIN sbagliato. Hai altri %1$d tentativi"</item>
</plurals>
<string name="screen_app_lock_biometric_authentication">"autenticazione biometrica"</string>
<string name="screen_app_lock_biometric_unlock">"sblocco biometrico"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Sblocca con la biometria"</string>
<string name="screen_app_lock_forgot_pin">"PIN dimenticato?"</string>
<string name="screen_app_lock_settings_change_pin">"Modifica il codice PIN"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Consenti lo sblocco biometrico"</string>
<string name="screen_app_lock_settings_remove_pin">"Rimuovi PIN"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Vuoi davvero rimuovere il PIN?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Rimuovere il PIN?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Consenti %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Preferisco usare il PIN"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Risparmia un po\' di tempo e usa %1$s per sbloccare l\'app ogni volta"</string>
<string name="screen_app_lock_setup_choose_pin">"Scegli il PIN"</string>
<string name="screen_app_lock_setup_confirm_pin">"Conferma il PIN"</string>
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Non puoi scegliere questo codice PIN per motivi di sicurezza"</string>
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Scegli un PIN diverso"</string>
<string name="screen_app_lock_setup_pin_context">"Blocca %1$s per aggiungere ulteriore sicurezza alle tue chat.
Scegli qualcosa che puoi ricordare. Se dimentichi questo PIN, verrai disconnesso dall\'app."</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Inserisci lo stesso PIN due volte"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"I PIN non corrispondono"</string>
<string name="screen_app_lock_signout_alert_message">"Dovrai effettuare nuovamente l\'accesso e creare un nuovo PIN per procedere"</string>
<string name="screen_app_lock_signout_alert_title">"Stai per essere disconnesso"</string>
<string name="screen_app_lock_use_biometric_android">"Usa la biometria"</string>
<string name="screen_app_lock_use_pin_android">"Usa il PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Uscita in corso…"</string>
</resources>

View file

@ -11,11 +11,11 @@
<item quantity="many">"Неверный PIN-код. У вас остался %1$d шанса"</item>
</plurals>
<string name="screen_app_lock_biometric_authentication">"биометрическая идентификация"</string>
<string name="screen_app_lock_biometric_unlock">"биометрическая разблокировать"</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_enable_biometric_unlock">"Разрешить биометрическую разблокировку"</string>
<string name="screen_app_lock_settings_remove_pin">"Удалить PIN-код"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы действительно хотите удалить PIN-код?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Удалить PIN-код?"</string>

View file

@ -18,6 +18,8 @@ package io.element.android.features.login.impl.changeserver
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
@ -63,8 +65,11 @@ fun ChangeServerView(
}
}
is AsyncData.Loading -> ProgressDialog()
is AsyncData.Success -> LaunchedEffect(state.changeServerAction) {
onDone()
is AsyncData.Success -> {
val latestOnDone by rememberUpdatedState(onDone)
LaunchedEffect(state.changeServerAction) {
latestOnDone()
}
}
AsyncData.Uninitialized -> Unit
}

View file

@ -203,15 +203,17 @@ private fun LoginForm(
.onTabOrEnterKeyFocusNext(focusManager)
.testTag(TestTags.loginEmailUsername)
.autofill(autofillTypes = listOf(AutofillType.Username), onFill = {
loginFieldState = it
eventSink(LoginPasswordEvents.SetLogin(it))
val sanitized = it.sanitize()
loginFieldState = sanitized
eventSink(LoginPasswordEvents.SetLogin(sanitized))
}),
placeholder = {
Text(text = stringResource(CommonStrings.common_username))
},
onValueChange = {
loginFieldState = it
eventSink(LoginPasswordEvents.SetLogin(it))
val sanitized = it.sanitize()
loginFieldState = sanitized
eventSink(LoginPasswordEvents.SetLogin(sanitized))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
@ -233,7 +235,6 @@ private fun LoginForm(
null
},
)
var passwordVisible by remember { mutableStateOf(false) }
if (state.loginAction is AsyncData.Loading) {
// Ensure password is hidden when user submits the form
@ -248,12 +249,14 @@ private fun LoginForm(
.onTabOrEnterKeyFocusNext(focusManager)
.testTag(TestTags.loginPassword)
.autofill(autofillTypes = listOf(AutofillType.Password), onFill = {
passwordFieldState = it
eventSink(LoginPasswordEvents.SetPassword(it))
val sanitized = it.sanitize()
passwordFieldState = sanitized
eventSink(LoginPasswordEvents.SetPassword(sanitized))
}),
onValueChange = {
passwordFieldState = it
eventSink(LoginPasswordEvents.SetPassword(it))
val sanitized = it.sanitize()
passwordFieldState = sanitized
eventSink(LoginPasswordEvents.SetPassword(sanitized))
},
placeholder = {
Text(text = stringResource(CommonStrings.common_password))
@ -281,6 +284,13 @@ private fun LoginForm(
}
}
/**
* Ensure that the string does not contain any new line characters, which can happen when pasting values.
*/
private fun String.sanitize(): String {
return replace("\n", "")
}
@Composable
private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
ErrorDialog(

View file

@ -7,7 +7,7 @@
<string name="screen_account_provider_form_title">"Fiókszolgáltató keresése"</string>
<string name="screen_account_provider_signin_subtitle">"Itt lesznek a beszélgetései ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."</string>
<string name="screen_account_provider_signin_title">"Hamarosan bejelentkezik ide: %s"</string>
<string name="screen_account_provider_signup_subtitle">"Itt lesznek a beszélgetései ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."</string>
<string name="screen_account_provider_signup_subtitle">"Itt lesznek a beszélgetéseid ahogyan egy e-mail-szolgáltatást is használnál a leveleid kezeléséhez."</string>
<string name="screen_account_provider_signup_title">"Hamarosan létrehoz egy fiókot itt: %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"A Matrix.org egy nagy, ingyenes kiszolgáló a nyilvános Matrix-hálózaton, a biztonságos, decentralizált kommunikáció érdekében, amelyet a Matrix.org Alapítvány üzemeltet."</string>
<string name="screen_change_account_provider_other">"Egyéb"</string>

View file

@ -1,14 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Cambia fornitore dell\'account"</string>
<string name="screen_account_provider_form_hint">"Indirizzo dell\'homeserver"</string>
<string name="screen_account_provider_form_notice">"Inserisci un termine di ricerca o un indirizzo di dominio."</string>
<string name="screen_account_provider_form_subtitle">"Cerca un\' azienda, una comunità o un server privato."</string>
<string name="screen_account_provider_form_title">"Trova un fornitore di account"</string>
<string name="screen_account_provider_signin_subtitle">"Qui è dove vivranno le tue conversazioni - proprio come useresti un fornitore di posta elettronica per conservare le tue email."</string>
<string name="screen_account_provider_signin_title">"Stai per accedere a %s"</string>
<string name="screen_account_provider_signup_subtitle">"Qui è dove vivranno le tue conversazioni - proprio come useresti un fornitore di posta elettronica per conservare le tue email."</string>
<string name="screen_account_provider_signup_title">"Stai per creare un account su %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org è un grande server gratuito nella rete pubblica Matrix per una comunicazione sicura e decentralizzata, gestito dalla Fondazione Matrix.org."</string>
<string name="screen_change_account_provider_other">"Altro"</string>
<string name="screen_change_account_provider_subtitle">"Utilizza un provider di account diverso, ad esempio il tuo server privato o un account di lavoro."</string>
<string name="screen_change_account_provider_title">"Cambia fornitore dell\'account"</string>
<string name="screen_change_server_error_invalid_homeserver">"Non siamo riusciti a raggiungere questo homserver. Verifica di aver inserito correttamente l\'URL del server domestico. Se l\'URL è corretto, contatta l\'amministratore del tuo server domestico per ulteriore assistenza."</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Questo server attualmente non supporta la sincronizzazione scorrevole."</string>
<string name="screen_change_server_form_header">"URL dell\'homeserver"</string>
<string name="screen_change_server_form_notice">"Puoi connetterti solo a un server esistente che supporta la sincronizzazione scorrevole. L\'amministratore del tuo server domestico dovrà configurarlo. %1$s"</string>
<string name="screen_change_server_subtitle">"Qual è l\'indirizzo del tuo server?"</string>
<string name="screen_change_server_title">"Seleziona il tuo server"</string>
<string name="screen_login_error_deactivated_account">"Questo profilo è stato disattivato."</string>
<string name="screen_login_error_invalid_credentials">"Nome utente e/o password errati"</string>
<string name="screen_login_error_invalid_user_id">"Questo non è un identificatore utente valido. Formato previsto: \'@user:homeserver.org\'"</string>
<string name="screen_login_error_unsupported_authentication">"L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver."</string>
<string name="screen_login_form_header">"Inserisci i tuoi dati"</string>
<string name="screen_login_title">"Bentornato!"</string>
<string name="screen_login_title_with_homeserver">"Accedi a %1$s"</string>
<string name="screen_server_confirmation_change_server">"Cambia fornitore dell\'account"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Un server privato per i dipendenti di Element."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix è una rete aperta per comunicazioni sicure e decentralizzate."</string>
<string name="screen_server_confirmation_message_register">"Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email."</string>
<string name="screen_server_confirmation_title_login">"Stai per accedere a %1$s"</string>
<string name="screen_server_confirmation_title_register">"Stai per creare un account su %1$s"</string>
<string name="screen_waitlist_message">"Al momento c\'è una grande richiesta per %1$s su %2$s. Torna a visitare l\'app tra qualche giorno e riprova.
Grazie per la pazienza!"</string>
<string name="screen_waitlist_title">"Ci sei quasi."</string>
<string name="screen_waitlist_title_success">"Sei dentro."</string>
<string name="screen_login_subtitle">"Matrix è una rete aperta per comunicazioni sicure e decentralizzate."</string>
<string name="screen_waitlist_message_success">"Benvenuti in %1$s!"</string>
</resources>

View file

@ -18,6 +18,8 @@ package io.element.android.features.logout.impl.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.res.stringResource
import io.element.android.features.logout.impl.R
import io.element.android.libraries.architecture.AsyncAction
@ -52,9 +54,11 @@ fun LogoutActionDialog(
onRetry = onForceLogoutClicked,
onDismiss = onDismissError,
)
is AsyncAction.Success ->
is AsyncAction.Success -> {
val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout)
LaunchedEffect(state) {
onSuccessLogout(state.data)
latestOnSuccessLogout(state.data)
}
}
}
}

View file

@ -2,4 +2,17 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Sei sicuro di voler uscire?"</string>
<string name="screen_signout_in_progress_dialog_content">"Uscita in corso…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati."</string>
<string name="screen_signout_key_backup_disabled_title">"Hai disattivato il backup"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Il backup delle chiavi era ancora in corso quando sei andato offline. Riconnettiti per eseguire il backup delle chiavi prima di uscire."</string>
<string name="screen_signout_key_backup_offline_title">"Il backup delle chiavi è ancora in corso"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Attendi il completamento dell\'operazione prima di uscire."</string>
<string name="screen_signout_key_backup_ongoing_title">"Il backup delle chiavi è ancora in corso"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati."</string>
<string name="screen_signout_recovery_disabled_title">"Recupero non impostato"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Stai per disconnettere la tua ultima sessione. Se esci ora, potresti perdere l\'accesso ai tuoi messaggi cifrati."</string>
<string name="screen_signout_save_recovery_key_title">"Hai salvato la chiave di recupero?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Disconnetti"</string>
<string name="screen_signout_confirmation_dialog_title">"Disconnetti"</string>
<string name="screen_signout_preference_item">"Disconnetti"</string>
</resources>

View file

@ -63,7 +63,7 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
@ -81,10 +81,10 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canRedactAsState
import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState
import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import kotlinx.coroutines.CoroutineScope
@ -107,12 +107,11 @@ class MessagesPresenter @AssistedInject constructor(
private val messageSummaryFormatter: MessageSummaryFormatter,
private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper,
private val preferencesStore: PreferencesStore,
private val appPreferencesStore: AppPreferencesStore,
private val featureFlagsService: FeatureFlagService,
private val htmlConverterProvider: HtmlConverterProvider,
@Assisted private val navigator: MessagesNavigator,
private val buildMeta: BuildMeta,
private val currentSessionIdHolder: CurrentSessionIdHolder,
) : Presenter<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
@ -123,7 +122,7 @@ class MessagesPresenter @AssistedInject constructor(
@Composable
override fun present(): MessagesState {
htmlConverterProvider.Update(currentUserId = currentSessionIdHolder.current)
htmlConverterProvider.Update(currentUserId = room.sessionId)
val roomInfo by room.roomInfoFlow.collectAsState(null)
val localCoroutineScope = rememberCoroutineScope()
@ -138,7 +137,8 @@ class MessagesPresenter @AssistedInject constructor(
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
val userHasPermissionToRedactOwn by room.canRedactOwnAsState(updateKey = syncUpdateFlow.value)
val userHasPermissionToRedactOther by room.canRedactOtherAsState(updateKey = syncUpdateFlow.value)
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, updateKey = syncUpdateFlow.value)
val roomName: AsyncData<String> by remember {
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
@ -155,15 +155,15 @@ class MessagesPresenter @AssistedInject constructor(
mutableStateOf(false)
}
LaunchedEffect(currentSessionIdHolder.current) {
LaunchedEffect(syncUpdateFlow.value) {
withContext(dispatchers.io) {
canJoinCall = room.canUserJoinCall(userId = currentSessionIdHolder.current).getOrDefault(false)
canJoinCall = room.canUserJoinCall(room.sessionId).getOrDefault(false)
}
}
val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow.value) {
withContext(dispatchers.io) {
showReinvitePrompt = !hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L
}
@ -176,7 +176,7 @@ class MessagesPresenter @AssistedInject constructor(
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
}
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
val enableTextFormatting by appPreferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
var enableVoiceMessages by remember { mutableStateOf(false) }
LaunchedEffect(featureFlagsService) {
@ -219,7 +219,8 @@ class MessagesPresenter @AssistedInject constructor(
roomName = roomName,
roomAvatar = roomAvatar,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToRedact = userHasPermissionToRedact,
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
composerState = composerState,
voiceMessageComposerState = voiceMessageComposerState,
@ -312,7 +313,7 @@ class MessagesPresenter @AssistedInject constructor(
}
}
private suspend fun handleActionEdit(
private fun handleActionEdit(
targetEvent: TimelineItem.Event,
composerState: MessageComposerState,
enableTextFormatting: Boolean,

View file

@ -36,7 +36,8 @@ data class MessagesState(
val roomName: AsyncData<String>,
val roomAvatar: AsyncData<AvatarData>,
val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToRedact: Boolean,
val userHasPermissionToRedactOwn: Boolean,
val userHasPermissionToRedactOther: Boolean,
val userHasPermissionToSendReaction: Boolean,
val composerState: MessageComposerState,
val voiceMessageComposerState: VoiceMessageComposerState,

View file

@ -86,7 +86,8 @@ fun aMessagesState() = MessagesState(
roomName = AsyncData.Success("Room name"),
roomAvatar = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
userHasPermissionToSendMessage = true,
userHasPermissionToRedact = false,
userHasPermissionToRedactOwn = false,
userHasPermissionToRedactOther = false,
userHasPermissionToSendReaction = true,
composerState = aMessageComposerState().copy(
richTextEditorState = RichTextEditorState("Hello", initialFocus = true),

View file

@ -41,8 +41,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@ -160,7 +162,8 @@ fun MessagesView(
state.actionListState.eventSink(
ActionListEvents.ComputeForMessage(
event = event,
canRedact = state.userHasPermissionToRedact,
canRedactOwn = state.userHasPermissionToRedactOwn,
canRedactOther = state.userHasPermissionToRedactOther,
canSendMessage = state.userHasPermissionToSendMessage,
canSendReaction = state.userHasPermissionToSendReaction,
)
@ -293,8 +296,11 @@ private fun AttachmentStateView(
) {
when (state) {
AttachmentsState.None -> Unit
is AttachmentsState.Previewing -> LaunchedEffect(state) {
onPreviewAttachments(state.attachments)
is AttachmentsState.Previewing -> {
val latestOnPreviewAttachments by rememberUpdatedState(onPreviewAttachments)
LaunchedEffect(state) {
latestOnPreviewAttachments(state.attachments)
}
}
is AttachmentsState.Sending -> {
ProgressDialog(

View file

@ -22,7 +22,8 @@ sealed interface ActionListEvents {
data object Clear : ActionListEvents
data class ComputeForMessage(
val event: TimelineItem.Event,
val canRedact: Boolean,
val canRedactOwn: Boolean,
val canRedactOther: Boolean,
val canSendMessage: Boolean,
val canSendReaction: Boolean,
) : ActionListEvents

View file

@ -31,7 +31,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
import io.element.android.features.messages.impl.timeline.model.event.canReact
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
class ActionListPresenter @Inject constructor(
private val preferencesStore: PreferencesStore,
private val appPreferencesStore: AppPreferencesStore,
) : Presenter<ActionListState> {
@Composable
override fun present(): ActionListState {
@ -49,14 +49,15 @@ class ActionListPresenter @Inject constructor(
mutableStateOf(ActionListState.Target.None)
}
val isDeveloperModeEnabled by preferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
fun handleEvents(event: ActionListEvents) {
when (event) {
ActionListEvents.Clear -> target.value = ActionListState.Target.None
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
timelineItem = event.event,
userCanRedact = event.canRedact,
userCanRedactOwn = event.canRedactOwn,
userCanRedactOther = event.canRedactOther,
userCanSendMessage = event.canSendMessage,
userCanSendReaction = event.canSendReaction,
isDeveloperModeEnabled = isDeveloperModeEnabled,
@ -73,13 +74,15 @@ class ActionListPresenter @Inject constructor(
private fun CoroutineScope.computeForMessage(
timelineItem: TimelineItem.Event,
userCanRedact: Boolean,
userCanRedactOwn: Boolean,
userCanRedactOther: Boolean,
userCanSendMessage: Boolean,
userCanSendReaction: Boolean,
isDeveloperModeEnabled: Boolean,
target: MutableState<ActionListState.Target>
) = launch {
target.value = ActionListState.Target.Loading(timelineItem)
val canRedact = timelineItem.isMine && userCanRedactOwn || !timelineItem.isMine && userCanRedactOther
val actions =
when (timelineItem.content) {
is TimelineItemRedactedContent -> {
@ -98,8 +101,10 @@ class ActionListPresenter @Inject constructor(
}
}
is TimelineItemPollContent -> {
val canEndPoll = timelineItem.isRemote &&
!timelineItem.content.isEnded &&
(timelineItem.isMine || canRedact)
buildList {
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
if (timelineItem.isRemote) {
// Can only reply or forward messages already uploaded to the server
add(TimelineItemAction.Reply)
@ -107,7 +112,7 @@ class ActionListPresenter @Inject constructor(
if (timelineItem.isRemote && timelineItem.isEditable) {
add(TimelineItemAction.Edit)
}
if (timelineItem.isRemote && !timelineItem.content.isEnded && isMineOrCanRedact) {
if (canEndPoll) {
add(TimelineItemAction.EndPoll)
}
if (timelineItem.content.canBeCopied()) {
@ -119,7 +124,7 @@ class ActionListPresenter @Inject constructor(
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (isMineOrCanRedact) {
if (canRedact) {
add(TimelineItemAction.Redact)
}
}
@ -136,7 +141,7 @@ class ActionListPresenter @Inject constructor(
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (timelineItem.isMine || userCanRedact) {
if (canRedact) {
add(TimelineItemAction.Redact)
}
}
@ -169,7 +174,7 @@ class ActionListPresenter @Inject constructor(
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (timelineItem.isMine || userCanRedact) {
if (canRedact) {
add(TimelineItemAction.Redact)
}
}

View file

@ -24,6 +24,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -57,8 +59,9 @@ fun AttachmentsPreviewView(
}
if (state.sendActionState is SendActionState.Done) {
val latestOnDismiss by rememberUpdatedState(onDismiss)
LaunchedEffect(state.sendActionState) {
onDismiss()
latestOnDismiss()
}
}

View file

@ -18,9 +18,6 @@ package io.element.android.features.messages.impl.forward
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -51,30 +48,3 @@ fun aForwardMessagesState(
forwardingSucceeded = forwardingSucceeded,
eventSink = {}
)
internal fun aForwardMessagesRoomList() = persistentListOf(
aRoomDetailsState(),
aRoomDetailsState(roomId = RoomId("!room2:domain"), canonicalAlias = "#element-x-room:matrix.org"),
)
fun aRoomDetailsState(
roomId: RoomId = RoomId("!room:domain"),
name: String = "roomName",
canonicalAlias: String? = null,
isDirect: Boolean = true,
avatarURLString: String? = null,
lastMessage: RoomMessage? = null,
lastMessageTimestamp: Long? = null,
unreadNotificationCount: Int = 0,
inviter: RoomMember? = null,
) = RoomSummaryDetails(
roomId = roomId,
name = name,
canonicalAlias = canonicalAlias,
isDirect = isDirect,
avatarURLString = avatarURLString,
lastMessage = lastMessage,
lastMessageTimestamp = lastMessageTimestamp,
unreadNotificationCount = unreadNotificationCount,
inviter = inviter,
)

View file

@ -43,6 +43,7 @@ sealed interface MessageComposerEvents {
data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents
data object CancelSendAttachment : MessageComposerEvents
data class Error(val error: Throwable) : MessageComposerEvents
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
}

View file

@ -20,6 +20,7 @@ import android.Manifest
import android.annotation.SuppressLint
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@ -183,9 +184,8 @@ class MessageComposerPresenter @Inject constructor(
val currentUserId = currentSessionIdHolder.current
suspend fun canSendRoomMention(): Boolean {
val roomIsDm = room.isDirect && room.isOneToOne
val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false)
return !roomIsDm && userCanSendAtRoom
return !room.isDm && userCanSendAtRoom
}
// This will trigger a search immediately when `@` is typed
@ -208,6 +208,15 @@ class MessageComposerPresenter @Inject constructor(
.collect()
}
DisposableEffect(Unit) {
// Declare that the user is not typing anymore when the composer is disposed
onDispose {
appCoroutineScope.launch {
room.typingNotice(false)
}
}
}
fun handleEvents(event: MessageComposerEvents) {
when (event) {
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
@ -300,6 +309,11 @@ class MessageComposerPresenter @Inject constructor(
is MessageComposerEvents.Error -> {
analyticsService.trackError(event.error)
}
is MessageComposerEvents.TypingNotice -> {
localCoroutineScope.launch {
room.typingNotice(event.isTyping)
}
}
is MessageComposerEvents.SuggestionReceived -> {
suggestionSearchTrigger.value = event.suggestion
}

View file

@ -78,6 +78,10 @@ internal fun MessageComposerView(
state.eventSink(MessageComposerEvents.Error(error))
}
fun onTyping(typing: Boolean) {
state.eventSink(MessageComposerEvents.TypingNotice(typing))
}
val coroutineScope = rememberCoroutineScope()
fun onRequestFocus() {
coroutineScope.launch {
@ -121,6 +125,7 @@ internal fun MessageComposerView(
onDeleteVoiceMessage = onDeleteVoiceMessage,
onSuggestionReceived = ::onSuggestionReceived,
onError = ::onError,
onTyping = ::onTyping,
currentUserId = state.currentUserId,
onRichContentSelected = ::sendUri,
)

View file

@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.timeline.session.SessionState
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId
@ -53,6 +54,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -72,6 +74,7 @@ class TimelinePresenter @AssistedInject constructor(
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
private val sendPollResponseAction: SendPollResponseAction,
private val endPollAction: EndPollAction,
private val sessionPreferencesStore: SessionPreferencesStore,
) : Presenter<TimelineState> {
@AssistedFactory
interface Factory {
@ -102,6 +105,8 @@ class TimelinePresenter @AssistedInject constructor(
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
val sessionState by remember {
derivedStateOf {
SessionState(
@ -111,8 +116,6 @@ class TimelinePresenter @AssistedInject constructor(
}
}
val membersState by room.membersStateFlow.collectAsState()
fun handleEvents(event: TimelineEvents) {
when (event) {
TimelineEvents.LoadMore -> localScope.paginateBackwards()
@ -125,7 +128,8 @@ class TimelinePresenter @AssistedInject constructor(
firstVisibleIndex = event.firstIndex,
timelineItems = timelineItems,
lastReadReceiptIndex = lastReadReceiptIndex,
lastReadReceiptId = lastReadReceiptId
lastReadReceiptId = lastReadReceiptId,
readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE,
)
}
is TimelineEvents.PollAnswerSelected -> appScope.launch {
@ -149,13 +153,12 @@ class TimelinePresenter @AssistedInject constructor(
}
LaunchedEffect(Unit) {
timeline
.timelineItems
.onEach {
combine(timeline.timelineItems, room.membersStateFlow) { items, membersState ->
timelineItemsFactory.replaceWith(
timelineItems = it,
timelineItems = items,
roomMembers = membersState.roomMembers().orEmpty()
)
items
}
.onEach { timelineItems ->
if (timelineItems.isEmpty()) {
@ -225,13 +228,14 @@ class TimelinePresenter @AssistedInject constructor(
timelineItems: ImmutableList<TimelineItem>,
lastReadReceiptIndex: MutableState<Int>,
lastReadReceiptId: MutableState<EventId?>,
readReceiptType: ReceiptType,
) = launch(dispatchers.computation) {
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) {
lastReadReceiptIndex.value = firstVisibleIndex
lastReadReceiptId.value = eventId
timeline.sendReadReceipt(eventId = eventId, receiptType = ReceiptType.READ)
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
}
}

View file

@ -42,6 +42,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -145,7 +146,7 @@ fun TimelineView(
}
}
}
if (state.paginationState.beginningOfRoomReached) {
if (state.paginationState.beginningOfRoomReached && !state.timelineRoomInfo.isDirect) {
item(contentType = "BeginningOfRoomReached") {
TimelineItemRoomBeginningView(roomName = roomName)
}
@ -193,10 +194,11 @@ private fun BoxScope.TimelineScrollHelper(
}
}
val latestOnScrollFinishedAt by rememberUpdatedState(onScrollFinishedAt)
LaunchedEffect(isScrollFinished, isTimelineEmpty) {
if (isScrollFinished && !isTimelineEmpty) {
// Notify the parent composable about the first visible item index when scrolling finishes
onScrollFinishedAt(lazyListState.firstVisibleItemIndex)
latestOnScrollFinishedAt(lazyListState.firstVisibleItemIndex)
}
}

View file

@ -18,10 +18,11 @@ package io.element.android.features.messages.impl.timeline.components.layout
import android.text.Layout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.unit.Constraints
@ -59,23 +60,27 @@ fun ContentAvoidingLayout(
) {
val scope = remember { ContentAvoidingLayoutScopeInstance() }
SubcomposeLayout(
Layout(
modifier = modifier,
) { constraints ->
content = {
scope.content()
overlay()
}
) { measurables, constraints ->
// Measure the `overlay` view first, in case we need to shrink the `content`
val overlayPlaceable = subcompose(0, overlay).first().measure(Constraints(minWidth = 0, maxWidth = constraints.maxWidth))
val overlayPlaceable = measurables.last().measure(Constraints(minWidth = 0, maxWidth = constraints.maxWidth))
val contentConstraints = if (shrinkContent) {
Constraints(minWidth = 0, maxWidth = constraints.maxWidth - overlayPlaceable.width)
} else {
Constraints(minWidth = 0, maxWidth = constraints.maxWidth)
}
val contentPlaceable = subcompose(1) { scope.content() }.first().measure(contentConstraints)
val contentPlaceable = measurables.first().measure(contentConstraints)
var layoutWidth = contentPlaceable.width
var layoutHeight = contentPlaceable.height
val data = scope.data
val data = scope.data.value
// Free space = width of the whole component - width of its non overlapping contents
val freeSpace = max(contentPlaceable.width - data.nonOverlappingContentWidth, 0)
@ -135,13 +140,10 @@ interface ContentAvoidingLayoutScope {
}
private class ContentAvoidingLayoutScopeInstance(
val data: ContentAvoidingLayoutData = ContentAvoidingLayoutData(),
val data: MutableState<ContentAvoidingLayoutData> = mutableStateOf(ContentAvoidingLayoutData()),
) : ContentAvoidingLayoutScope {
override fun onContentLayoutChanged(data: ContentAvoidingLayoutData) {
this.data.contentWidth = data.contentWidth
this.data.contentHeight = data.contentHeight
this.data.nonOverlappingContentWidth = data.nonOverlappingContentWidth
this.data.nonOverlappingContentHeight = data.nonOverlappingContentHeight
this.data.value = data
}
}

View file

@ -87,7 +87,16 @@ class TimelineItemsFactory @Inject constructor(
newTimelineItemStates.add(timelineItemState)
}
} else {
newTimelineItemStates.add(cacheItem)
val updatedItem = if (cacheItem is TimelineItem.Event && roomMembers.isNotEmpty()) {
eventItemFactory.update(
timelineItem = cacheItem,
receivedMatrixTimelineItem = timelineItems[index] as MatrixTimelineItem.Event,
roomMembers = roomMembers
)
} else {
cacheItem
}
newTimelineItemStates.add(updatedItem)
}
}
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()

View file

@ -24,13 +24,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import javax.inject.Inject
class TimelineItemContentFactory @Inject constructor(
@ -50,7 +50,7 @@ class TimelineItemContentFactory @Inject constructor(
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
is MessageContent -> {
val senderDisplayName = (eventTimelineItem.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: eventTimelineItem.sender.value
val senderDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender)
messageFactory.create(itemContent, senderDisplayName, eventTimelineItem.eventId)
}
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)

View file

@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import kotlinx.collections.immutable.toImmutableList
import java.text.DateFormat
import java.util.Date
@ -52,21 +53,7 @@ class TimelineItemEventFactory @Inject constructor(
val currentSender = currentTimelineItem.event.sender
val groupPosition =
computeGroupPosition(currentTimelineItem, timelineItems, index)
val senderDisplayName: String?
val senderAvatarUrl: String?
when (val senderProfile = currentTimelineItem.event.senderProfile) {
ProfileTimelineDetails.Unavailable,
ProfileTimelineDetails.Pending,
is ProfileTimelineDetails.Error -> {
senderDisplayName = null
senderAvatarUrl = null
}
is ProfileTimelineDetails.Ready -> {
senderDisplayName = senderProfile.displayName
senderAvatarUrl = senderProfile.avatarUrl
}
}
val (senderDisplayName, senderAvatarUrl) = currentTimelineItem.getSenderInfo()
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
@ -100,6 +87,36 @@ class TimelineItemEventFactory @Inject constructor(
)
}
fun update(
timelineItem: TimelineItem.Event,
receivedMatrixTimelineItem: MatrixTimelineItem.Event,
roomMembers: List<RoomMember>,
): TimelineItem.Event {
return timelineItem.copy(
readReceiptState = receivedMatrixTimelineItem.computeReadReceiptState(roomMembers)
)
}
private fun MatrixTimelineItem.Event.getSenderInfo(): Pair<String?, String?> {
val senderDisplayName: String?
val senderAvatarUrl: String?
when (val senderProfile = event.senderProfile) {
ProfileTimelineDetails.Unavailable,
ProfileTimelineDetails.Pending,
is ProfileTimelineDetails.Error -> {
senderDisplayName = null
senderAvatarUrl = null
}
is ProfileTimelineDetails.Ready -> {
senderDisplayName = senderProfile.getDisambiguatedDisplayName(event.sender)
senderAvatarUrl = senderProfile.avatarUrl
}
}
return senderDisplayName to senderAvatarUrl
}
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
var aggregatedReactions = event.reactions.map { reaction ->

View file

@ -13,6 +13,16 @@
<item quantity="few">"%1$d změny místnosti"</item>
<item quantity="other">"%1$d změn místnosti"</item>
</plurals>
<plurals name="screen_room_typing_many_members">
<item quantity="one">"%1$s, %2$s a %3$d další"</item>
<item quantity="few">"%1$s, %2$s a %3$d další"</item>
<item quantity="other">"%1$s, %2$s a %3$d dalších"</item>
</plurals>
<plurals name="screen_room_typing_notification">
<item quantity="one">"%1$s píše"</item>
<item quantity="few">"%1$s píší"</item>
<item quantity="other">"%1$s píše"</item>
</plurals>
<string name="report_content_explanation">"Tato zpráva bude nahlášena správci vašeho domovského serveru. Nebude si moci přečíst žádné šifrované zprávy."</string>
<string name="report_content_hint">"Důvod nahlášení tohoto obsahu"</string>
<string name="room_timeline_beginning_of_room">"Toto je začátek %1$s."</string>
@ -54,6 +64,7 @@
<string name="screen_room_retry_send_menu_title">"Vaši zprávu se nepodařilo odeslat"</string>
<string name="screen_room_timeline_add_reaction">"Přidat emoji"</string>
<string name="screen_room_timeline_less_reactions">"Zobrazit méně"</string>
<string name="screen_room_typing_two_members">"%1$s a %2$s"</string>
<string name="screen_room_voice_message_tooltip">"Držte pro nahrávání"</string>
<string name="screen_room_mentions_at_room_title">"Všichni"</string>
<string name="screen_report_content_block_user">"Zablokovat uživatele"</string>

View file

@ -20,7 +20,7 @@
<string name="screen_room_mentions_at_room_subtitle">"Den ganzen Raum benachrichtigen"</string>
<string name="screen_report_content_block_user_hint">"Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"</string>
<string name="screen_room_attachment_source_camera">"Kamera"</string>
<string name="screen_room_attachment_source_camera_photo">"Foto machen"</string>
<string name="screen_room_attachment_source_camera_photo">"Foto aufnehmen"</string>
<string name="screen_room_attachment_source_camera_video">"Video aufnehmen"</string>
<string name="screen_room_attachment_source_files">"Anhang"</string>
<string name="screen_room_attachment_source_gallery">"Foto- und Videobibliothek"</string>

View file

@ -12,6 +12,14 @@
<item quantity="one">"%1$d changement dans le salon"</item>
<item quantity="other">"%1$d changements dans le salon"</item>
</plurals>
<plurals name="screen_room_typing_many_members">
<item quantity="one">"%1$s, %2$s et %3$d autre"</item>
<item quantity="other">"%1$s, %2$s et %3$d autres"</item>
</plurals>
<plurals name="screen_room_typing_notification">
<item quantity="one">"%1$s écrit"</item>
<item quantity="other">"%1$s écrivent"</item>
</plurals>
<string name="report_content_explanation">"Ce message sera signalé à ladministrateur de votre serveur daccueil. Il ne pourra lire aucun message chiffré."</string>
<string name="report_content_hint">"Raison du signalement de ce contenu"</string>
<string name="room_timeline_beginning_of_room">"Ceci est le début de %1$s."</string>
@ -23,7 +31,7 @@
<string name="screen_room_attachment_source_camera_photo">"Prendre une photo"</string>
<string name="screen_room_attachment_source_camera_video">"Enregistrer une vidéo"</string>
<string name="screen_room_attachment_source_files">"Pièce jointe"</string>
<string name="screen_room_attachment_source_gallery">"Gallerie Photo et Vidéo"</string>
<string name="screen_room_attachment_source_gallery">"Galerie Photo et Vidéo"</string>
<string name="screen_room_attachment_source_location">"Position"</string>
<string name="screen_room_attachment_source_poll">"Sondage"</string>
<string name="screen_room_attachment_text_formatting">"Formatage du texte"</string>
@ -53,6 +61,7 @@
<string name="screen_room_retry_send_menu_title">"Votre message na pas pu être envoyé"</string>
<string name="screen_room_timeline_add_reaction">"Ajouter un émoji"</string>
<string name="screen_room_timeline_less_reactions">"Afficher moins"</string>
<string name="screen_room_typing_two_members">"%1$s et %2$s"</string>
<string name="screen_room_voice_message_tooltip">"Maintenir pour enregistrer"</string>
<string name="screen_room_mentions_at_room_title">"Tout le monde"</string>
<string name="screen_report_content_block_user">"Bloquer lutilisateur"</string>

View file

@ -17,6 +17,45 @@
<string name="room_timeline_beginning_of_room">"Questo è l\'inizio di %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"Questo è l\'inizio della conversazione."</string>
<string name="room_timeline_read_marker_title">"Nuovo"</string>
<string name="screen_room_mentions_at_room_subtitle">"Avvisa l\'intera stanza"</string>
<string name="screen_report_content_block_user_hint">"Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente"</string>
<string name="screen_room_attachment_source_camera">"Fotocamera"</string>
<string name="screen_room_attachment_source_camera_photo">"Scatta foto"</string>
<string name="screen_room_attachment_source_camera_video">"Registra video"</string>
<string name="screen_room_attachment_source_files">"Allegato"</string>
<string name="screen_room_attachment_source_gallery">"Libreria di foto e video"</string>
<string name="screen_room_attachment_source_location">"Posizione"</string>
<string name="screen_room_attachment_source_poll">"Sondaggio"</string>
<string name="screen_room_attachment_text_formatting">"Formattazione del testo"</string>
<string name="screen_room_encrypted_history_banner">"La cronologia dei messaggi non è attualmente disponibile."</string>
<string name="screen_room_encrypted_history_banner_unverified">"La cronologia dei messaggi non è disponibile in questa stanza. Verifica questo dispositivo per vedere la cronologia dei messaggi."</string>
<string name="screen_room_error_failed_retrieving_user_details">"Impossibile recuperare i dettagli dell\'utente"</string>
<string name="screen_room_invite_again_alert_message">"Vorresti invitarli di nuovo?"</string>
<string name="screen_room_invite_again_alert_title">"Ci sei solo tu in questa chat"</string>
<string name="screen_room_message_copied">"Messaggio copiato"</string>
<string name="screen_room_no_permission_to_post">"Non sei autorizzato a postare in questa stanza"</string>
<string name="screen_room_notification_settings_allow_custom">"Consenti impostazione personalizzata"</string>
<string name="screen_room_notification_settings_allow_custom_footnote">"L\'attivazione di questa opzione sovrascriverà l\'impostazione predefinita"</string>
<string name="screen_room_notification_settings_custom_settings_title">"Avvisami in questa chat per"</string>
<string name="screen_room_notification_settings_default_setting_footnote">"Puoi cambiarlo nelle tue %1$s."</string>
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"impostazioni globali"</string>
<string name="screen_room_notification_settings_default_setting_title">"Impostazione predefinita"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Rimuovi l\'impostazione personalizzata"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Si è verificato un errore durante il caricamento delle impostazioni di notifica."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Ripristino della modalità predefinita fallito, riprova."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Impossibile impostare la modalità, riprova."</string>
<string name="screen_room_notification_settings_mentions_only_disclaimer">"Il tuo homeserver non supporta questa opzione nelle stanze criptate, quindi non riceverai notifiche in questa stanza."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Tutti i messaggi"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"In questa stanza, avvisami per"</string>
<string name="screen_room_reactions_show_less">"Mostra meno"</string>
<string name="screen_room_reactions_show_more">"Mostra di più"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Invia di nuovo"</string>
<string name="screen_room_retry_send_menu_title">"Il tuo messaggio non è stato inviato"</string>
<string name="screen_room_timeline_add_reaction">"Aggiungi emoji"</string>
<string name="screen_room_timeline_less_reactions">"Mostra meno"</string>
<string name="screen_room_voice_message_tooltip">"Tieni premuto per registrare"</string>
<string name="screen_room_mentions_at_room_title">"Tutti"</string>
<string name="screen_report_content_block_user">"Blocca utente"</string>
<string name="screen_room_error_failed_processing_media">"Elaborazione del file multimediale da caricare fallita, riprova."</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Solo menzioni e parole chiave"</string>
</resources>

View file

@ -5,7 +5,7 @@
<string name="emoji_picker_category_foods">"Еда и напитки"</string>
<string name="emoji_picker_category_nature">"Животные и природа"</string>
<string name="emoji_picker_category_objects">"Объекты"</string>
<string name="emoji_picker_category_people">"Смайлы и люди"</string>
<string name="emoji_picker_category_people">"Улыбки и люди"</string>
<string name="emoji_picker_category_places">"Путешествия и места"</string>
<string name="emoji_picker_category_symbols">"Символы"</string>
<plurals name="room_timeline_state_changes">
@ -13,6 +13,16 @@
<item quantity="few">"%1$d изменения в комнате"</item>
<item quantity="many">"%1$d изменений в комнате"</item>
</plurals>
<plurals name="screen_room_typing_many_members">
<item quantity="one">"%1$s, %2$s и %3$d"</item>
<item quantity="few">"%1$s, %2$s и другие %3$d"</item>
<item quantity="many">"%1$s, %2$s и другие %3$d"</item>
</plurals>
<plurals name="screen_room_typing_notification">
<item quantity="one">"%1$s набирает сообщение"</item>
<item quantity="few">"%1$s набирают сообщения"</item>
<item quantity="many">"%1$s набирают сообщения"</item>
</plurals>
<string name="report_content_explanation">"Это сообщение будет передано администратору вашего домашнего сервера. Они не смогут прочитать зашифрованные сообщения."</string>
<string name="report_content_hint">"Причина, по которой вы пожаловались на этот контент"</string>
<string name="room_timeline_beginning_of_room">"Это начало %1$s."</string>
@ -54,6 +64,7 @@
<string name="screen_room_retry_send_menu_title">"Не удалось отправить ваше сообщение"</string>
<string name="screen_room_timeline_add_reaction">"Добавить эмодзи"</string>
<string name="screen_room_timeline_less_reactions">"Показать меньше"</string>
<string name="screen_room_typing_two_members">"%1$s и %2$s"</string>
<string name="screen_room_voice_message_tooltip">"Удерживайте для записи"</string>
<string name="screen_room_mentions_at_room_title">"Для всех"</string>
<string name="screen_report_content_block_user">"Заблокировать пользователя"</string>

View file

@ -12,6 +12,14 @@
<item quantity="one">"%1$d room change"</item>
<item quantity="other">"%1$d room changes"</item>
</plurals>
<plurals name="screen_room_typing_many_members">
<item quantity="one">"%1$s, %2$s and %3$d other"</item>
<item quantity="other">"%1$s, %2$s and %3$d others"</item>
</plurals>
<plurals name="screen_room_typing_notification">
<item quantity="one">"%1$s is typing"</item>
<item quantity="other">"%1$s are typing"</item>
</plurals>
<string name="report_content_explanation">"This message will be reported to your homeservers administrator. They will not be able to read any encrypted messages."</string>
<string name="report_content_hint">"Reason for reporting this content"</string>
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
@ -53,6 +61,7 @@
<string name="screen_room_retry_send_menu_title">"Your message failed to send"</string>
<string name="screen_room_timeline_add_reaction">"Add emoji"</string>
<string name="screen_room_timeline_less_reactions">"Show less"</string>
<string name="screen_room_typing_two_members">"%1$s and %2$s"</string>
<string name="screen_room_voice_message_tooltip">"Hold to record"</string>
<string name="screen_room_mentions_at_room_title">"Everyone"</string>
<string name="screen_report_content_block_user">"Block user"</string>

View file

@ -59,7 +59,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@ -120,7 +121,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.userHasPermissionToRedact).isFalse()
assertThat(initialState.userHasPermissionToRedactOwn).isFalse()
assertThat(initialState.hasNetworkConnection).isTrue()
assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
@ -601,14 +602,29 @@ class MessagesPresenterTest {
}
@Test
fun `present - permission to redact`() = runTest {
val matrixRoom = FakeMatrixRoom(canRedact = true)
fun `present - permission to redact own`() = runTest {
val matrixRoom = FakeMatrixRoom(canRedactOwn = true)
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedact }.last()
assertThat(initialState.userHasPermissionToRedact).isTrue()
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOwn }.last()
assertThat(initialState.userHasPermissionToRedactOwn).isTrue()
assertThat(initialState.userHasPermissionToRedactOther).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - permission to redact other`() = runTest {
val matrixRoom = FakeMatrixRoom(canRedactOther = true)
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedactOther }.last()
assertThat(initialState.userHasPermissionToRedactOwn).isFalse()
assertThat(initialState.userHasPermissionToRedactOther).isTrue()
cancelAndIgnoreRemainingEvents()
}
}
@ -649,10 +665,11 @@ class MessagesPresenterTest {
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
currentSessionIdHolder: CurrentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
): MessagesPresenter {
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
val appPreferencesStore = InMemoryAppPreferencesStore(isRichTextEditorEnabled = true)
val sessionPreferencesStore = InMemorySessionPreferencesStore()
val messageComposerPresenter = MessageComposerPresenter(
appCoroutineScope = this,
room = matrixRoom,
@ -687,14 +704,14 @@ class MessagesPresenterTest {
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
endPollAction = FakeEndPollAction(),
sendPollResponseAction = FakeSendPollResponseAction(),
sessionPreferencesStore = sessionPreferencesStore,
)
val timelinePresenterFactory = object : TimelinePresenter.Factory {
override fun create(navigator: MessagesNavigator): TimelinePresenter {
return timelinePresenter
}
}
val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true)
val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore)
val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore)
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
@ -714,11 +731,10 @@ class MessagesPresenterTest {
messageSummaryFormatter = FakeMessageSummaryFormatter(),
navigator = navigator,
clipboardHelper = clipboardHelper,
preferencesStore = preferencesStore,
appPreferencesStore = appPreferencesStore,
featureFlagsService = FakeFeatureFlagService(),
buildMeta = aBuildMeta(),
dispatchers = coroutineDispatchers,
currentSessionIdHolder = currentSessionIdHolder,
htmlConverterProvider = FakeHtmlConverterProvider(),
)
}

View file

@ -30,7 +30,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf
@ -38,6 +38,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@Suppress("LargeClass")
class ActionListPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@ -61,7 +62,15 @@ class ActionListPresenterTest {
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = false,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@ -87,7 +96,15 @@ class ActionListPresenterTest {
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = false,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@ -116,7 +133,15 @@ class ActionListPresenterTest {
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = false,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@ -149,7 +174,15 @@ class ActionListPresenterTest {
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = false, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = false,
canSendReaction = true
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@ -181,7 +214,15 @@ class ActionListPresenterTest {
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = false,
canRedactOther = true,
canSendMessage = true,
canSendReaction = true,
)
)
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@ -213,7 +254,15 @@ class ActionListPresenterTest {
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true, canSendReaction = false))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = false,
canRedactOther = true,
canSendMessage = true,
canSendReaction = false
)
)
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@ -245,7 +294,15 @@ class ActionListPresenterTest {
isMine = true,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@ -268,6 +325,47 @@ class ActionListPresenterTest {
}
}
@Test
fun `present - compute for my message cannot redact`() = runTest {
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = false,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
displayEmojiReactions = true,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Edit,
TimelineItemAction.Copy,
TimelineItemAction.ViewSource,
)
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
}
}
@Test
fun `present - compute for a media item`() = runTest {
val presenter = createActionListPresenter(isDeveloperModeEnabled = true)
@ -279,7 +377,15 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemImageContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@ -311,7 +417,15 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemStateEventContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = stateEvent,
canRedactOwn = false,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@ -341,7 +455,15 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemStateEventContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = stateEvent,
canRedactOwn = false,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@ -370,7 +492,15 @@ class ActionListPresenterTest {
isMine = true,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@ -408,10 +538,26 @@ class ActionListPresenterTest {
content = TimelineItemRedactedContent,
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = false,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = redactedEvent,
canRedactOwn = false,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
awaitItem().run {
assertThat(target).isEqualTo(ActionListState.Target.None)
}
@ -432,7 +578,15 @@ class ActionListPresenterTest {
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@ -460,7 +614,15 @@ class ActionListPresenterTest {
isEditable = true,
content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = false)),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@ -489,7 +651,15 @@ class ActionListPresenterTest {
isEditable = false,
content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = true)),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@ -517,7 +687,15 @@ class ActionListPresenterTest {
isEditable = false,
content = aTimelineItemPollContent(isEnded = true),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@ -543,7 +721,15 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemVoiceContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true, canSendReaction = true))
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
)
)
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@ -561,6 +747,6 @@ class ActionListPresenterTest {
}
private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter {
val preferencesStore = InMemoryPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
return ActionListPresenter(preferencesStore = preferencesStore)
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
return ActionListPresenter(appPreferencesStore = preferencesStore)
}

View file

@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
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.aRoomSummaryDetail
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
@ -54,7 +54,7 @@ class ForwardMessagesPresenterTests {
presenter.present()
}.test {
skipItems(1)
val summary = aRoomSummaryDetail()
val summary = aRoomSummaryDetails()
presenter.onRoomSelected(listOf(summary.roomId))
val forwardingState = awaitItem()
assertThat(forwardingState.isForwarding).isTrue()
@ -74,7 +74,7 @@ class ForwardMessagesPresenterTests {
// Test failed forwarding
room.givenForwardEventResult(Result.failure(Throwable("error")))
skipItems(1)
val summary = aRoomSummaryDetail()
val summary = aRoomSummaryDetails()
presenter.onRoomSelected(listOf(summary.roomId))
skipItems(1)
val failedForwardState = awaitItem()

View file

@ -873,6 +873,21 @@ class MessageComposerPresenterTest {
}
}
@Test
fun `present - handle typing notice event`() = runTest {
val room = FakeMatrixRoom()
val presenter = createPresenter(room = room, coroutineScope = this)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(room.typingRecord).isEmpty()
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(true))
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(false))
assertThat(room.typingRecord).isEqualTo(listOf(true, false))
}
}
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState {
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
skipItems(skipCount)

View file

@ -35,14 +35,20 @@ import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
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.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
@ -60,6 +66,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import java.util.Date
import kotlin.time.Duration.Companion.seconds
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
@ -129,13 +136,41 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
assertThat(timeline.sentReadReceipts).isEmpty()
val initialState = awaitFirstItem()
awaitWithLatch { latch ->
timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
}
assertThat(timeline.sendReadReceiptCount).isEqualTo(1)
assertThat(timeline.sentReadReceipts).isNotEmpty()
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - on scroll finished send a private read receipt if an event is before the index and public read receipts are disabled`() = runTest {
val timeline = FakeMatrixTimeline(
initialTimelineItems = listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
)
)
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
val presenter = createTimelinePresenter(
timeline = timeline,
sessionPreferencesStore = sessionPreferencesStore,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(timeline.sentReadReceipts).isEmpty()
val initialState = awaitFirstItem()
awaitWithLatch { latch ->
timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
}
assertThat(timeline.sentReadReceipts).isNotEmpty()
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE)
cancelAndIgnoreRemainingEvents()
}
}
@ -151,13 +186,13 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
assertThat(timeline.sentReadReceipts).isEmpty()
val initialState = awaitFirstItem()
awaitWithLatch { latch ->
timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
}
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
assertThat(timeline.sentReadReceipts).isEmpty()
cancelAndIgnoreRemainingEvents()
}
}
@ -173,13 +208,13 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
assertThat(timeline.sentReadReceipts).isEmpty()
val initialState = awaitFirstItem()
awaitWithLatch { latch ->
timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
}
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
assertThat(timeline.sentReadReceipts).isEmpty()
cancelAndIgnoreRemainingEvents()
}
}
@ -353,6 +388,50 @@ class TimelinePresenterTest {
}
}
@Test
fun `present - when room member info is loaded, read receipts info should be updated`() = runTest {
val timeline = FakeMatrixTimeline(
listOf(
MatrixTimelineItem.Event(
FAKE_UNIQUE_ID,
anEventTimelineItem(
sender = A_USER_ID,
receipts = persistentListOf(
Receipt(
userId = A_USER_ID,
timestamp = 0L,
)
)
)
)
)
)
val room = FakeMatrixRoom(matrixTimeline = timeline).apply {
givenRoomMembersState(MatrixRoomMembersState.Unknown)
}
val avatarUrl = "https://domain.com/avatar.jpg"
val presenter = createTimelinePresenter(timeline, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = consumeItemsUntilPredicate(30.seconds) { it.timelineItems.isNotEmpty() }.last()
val event = initialState.timelineItems.first() as TimelineItem.Event
assertThat(event.senderAvatar.url).isNull()
assertThat(event.readReceiptState.receipts.first().avatarData.url).isNull()
room.givenRoomMembersState(
MatrixRoomMembersState.Ready(
persistentListOf(aRoomMember(userId = A_USER_ID, avatarUrl = avatarUrl))
)
)
val updatedEvent = awaitItem().timelineItems.first() as TimelineItem.Event
assertThat(updatedEvent.readReceiptState.receipts.first().avatarData.url).isEqualTo(avatarUrl)
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
// Skip 1 item if Mentions feature is enabled
if (FeatureFlags.Mentions.defaultValue) {
@ -363,15 +442,17 @@ class TimelinePresenterTest {
private fun TestScope.createTimelinePresenter(
timeline: MatrixTimeline = FakeMatrixTimeline(),
room: FakeMatrixRoom = FakeMatrixRoom(matrixTimeline = timeline),
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
endPollAction: EndPollAction = FakeEndPollAction(),
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
): TimelinePresenter {
return TimelinePresenter(
timelineItemsFactory = timelineItemsFactory,
room = FakeMatrixRoom(matrixTimeline = timeline),
room = room,
dispatchers = testCoroutineDispatchers(),
appScope = this,
navigator = messagesNavigator,
@ -380,6 +461,7 @@ class TimelinePresenterTest {
redactedVoiceMessageManager = redactedVoiceMessageManager,
endPollAction = endPollAction,
sendPollResponseAction = sendPollResponseAction,
sessionPreferencesStore = sessionPreferencesStore,
)
}
}

View file

@ -33,5 +33,6 @@ interface OnBoardingEntryPoint : FeatureEntryPoint {
fun onSignUp()
fun onSignIn()
fun onOpenDeveloperSettings()
fun onReportProblem()
}
}

View file

@ -49,6 +49,10 @@ class OnBoardingNode @AssistedInject constructor(
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onOpenDeveloperSettings() }
}
private fun onReportProblem() {
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onReportProblem() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -59,6 +63,7 @@ class OnBoardingNode @AssistedInject constructor(
onCreateAccount = ::onSignUp,
onSignInWithQrCode = { /* Not supported yet */ },
onOpenDeveloperSettings = ::onOpenDeveloperSettings,
onReportProblem = ::onReportProblem,
)
}
}

View file

@ -16,6 +16,7 @@
package io.element.android.features.onboarding.impl
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -65,6 +66,7 @@ fun OnBoardingView(
onSignIn: () -> Unit,
onCreateAccount: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onReportProblem: () -> Unit,
modifier: Modifier = Modifier,
) {
OnBoardingPage(
@ -81,6 +83,7 @@ fun OnBoardingView(
onSignInWithQrCode = onSignInWithQrCode,
onSignIn = onSignIn,
onCreateAccount = onCreateAccount,
onReportProblem = onReportProblem,
)
}
)
@ -154,6 +157,7 @@ private fun OnBoardingButtons(
onSignInWithQrCode: () -> Unit,
onSignIn: () -> Unit,
onCreateAccount: () -> Unit,
onReportProblem: () -> Unit,
modifier: Modifier = Modifier,
) {
ButtonColumnMolecule(modifier = modifier) {
@ -186,7 +190,16 @@ private fun OnBoardingButtons(
.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(48.dp))
Spacer(modifier = Modifier.height(16.dp))
// Add a report problem text button. Use a Text since we need a special theme here.
Text(
modifier = Modifier
.padding(8.dp)
.clickable(onClick = onReportProblem),
text = stringResource(id = CommonStrings.common_report_a_problem),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
}
}
@ -200,6 +213,7 @@ internal fun OnBoardingScreenPreview(
onSignInWithQrCode = {},
onSignIn = {},
onCreateAccount = {},
onOpenDeveloperSettings = {}
onOpenDeveloperSettings = {},
onReportProblem = {},
)
}

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_onboarding_sign_in_manually">"Accedi manualmente"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Accedi con codice QR"</string>
<string name="screen_onboarding_sign_up">"Crea account"</string>
<string name="screen_onboarding_welcome_message">"Benvenuti nell\'Element più veloce di sempre. Potenziato per velocità e semplicità."</string>
<string name="screen_onboarding_welcome_subtitle">"Benvenuto su %1$s. Potenziato in velocità e semplicità."</string>
<string name="screen_onboarding_welcome_title">"Sii nel tuo elemento"</string>
</resources>

View file

@ -3,7 +3,7 @@
<string name="screen_onboarding_sign_in_manually">"Вход в систему вручную"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Войти с помощью QR-кода"</string>
<string name="screen_onboarding_sign_up">"Создать учетную запись"</string>
<string name="screen_onboarding_welcome_message">"Добро пожаловать в самый быстрый Element. Преимущество в скорости и простоте."</string>
<string name="screen_onboarding_welcome_subtitle">"Добро пожаловать в %1$s. Supercharged — это скорость и простота."</string>
<string name="screen_onboarding_welcome_title">"Будь c element"</string>
<string name="screen_onboarding_welcome_message">"Добро пожаловать в самый быстрый Element. Сверхзаряженность на скорость и простоту."</string>
<string name="screen_onboarding_welcome_subtitle">"Добро пожаловать в %1$s. Сверхзаряжен для скорости и простоты."</string>
<string name="screen_onboarding_welcome_title">"Будьте в своем element"</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">"Aggiungi opzione"</string>
<string name="screen_create_poll_anonymous_desc">"Mostra i risultati solo al termine del sondaggio"</string>
<string name="screen_create_poll_anonymous_headline">"Nascondi voti"</string>
<string name="screen_create_poll_answer_hint">"Opzione %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Le modifiche non sono state salvate. Vuoi davvero tornare indietro?"</string>
<string name="screen_create_poll_question_desc">"Domanda o argomento"</string>
<string name="screen_create_poll_question_hint">"Di cosa parla il sondaggio?"</string>
<string name="screen_create_poll_title">"Crea sondaggio"</string>
<string name="screen_edit_poll_delete_confirmation">"Vuoi davvero eliminare questo sondaggio?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Elimina sondaggio"</string>
<string name="screen_edit_poll_title">"Modifica sondaggio"</string>
<string name="screen_polls_history_empty_ongoing">"Impossibile trovare sondaggi in corso."</string>
<string name="screen_polls_history_empty_past">"Impossibile trovare sondaggi passati."</string>
<string name="screen_polls_history_filter_ongoing">"In corso"</string>
<string name="screen_polls_history_filter_past">"Passato"</string>
<string name="screen_polls_history_title">"Sondaggi"</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Добавить опцию"</string>
<string name="screen_create_poll_add_option_btn">"Добавить вариант"</string>
<string name="screen_create_poll_anonymous_desc">"Показывать результаты только после окончания опроса"</string>
<string name="screen_create_poll_anonymous_headline">"Анонимный опрос"</string>
<string name="screen_create_poll_answer_hint">"Настройка %1$d"</string>
@ -14,6 +14,6 @@
<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_filter_past">"Прошлые"</string>
<string name="screen_polls_history_title">"Опросы"</string>
</resources>

View file

@ -82,6 +82,7 @@ dependencies {
testImplementation(projects.libraries.indicator.impl)
testImplementation(projects.features.logout.impl)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)
testImplementation(projects.features.analytics.impl)
testImplementation(projects.tests.testutils)
}

View file

@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
sealed interface AdvancedSettingsEvents {
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetSendPublicReadReceiptsEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data object ChangeTheme : AdvancedSettingsEvents
data object CancelChangeTheme : AdvancedSettingsEvents
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents

View file

@ -25,40 +25,48 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.mapToTheme
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
import javax.inject.Inject
class AdvancedSettingsPresenter @Inject constructor(
private val preferencesStore: PreferencesStore,
private val appPreferencesStore: AppPreferencesStore,
private val sessionPreferencesStore: SessionPreferencesStore,
) : Presenter<AdvancedSettingsState> {
@Composable
override fun present(): AdvancedSettingsState {
val localCoroutineScope = rememberCoroutineScope()
val isRichTextEditorEnabled by preferencesStore
val isRichTextEditorEnabled by appPreferencesStore
.isRichTextEditorEnabledFlow()
.collectAsState(initial = false)
val isDeveloperModeEnabled by preferencesStore
val isDeveloperModeEnabled by appPreferencesStore
.isDeveloperModeEnabledFlow()
.collectAsState(initial = false)
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore
.isSendPublicReadReceiptsEnabled()
.collectAsState(initial = true)
val theme by remember {
preferencesStore.getThemeFlow().mapToTheme()
appPreferencesStore.getThemeFlow().mapToTheme()
}
.collectAsState(initial = Theme.System)
var showChangeThemeDialog by remember { mutableStateOf(false) }
fun handleEvents(event: AdvancedSettingsEvents) {
when (event) {
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
preferencesStore.setRichTextEditorEnabled(event.enabled)
appPreferencesStore.setRichTextEditorEnabled(event.enabled)
}
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
preferencesStore.setDeveloperModeEnabled(event.enabled)
appPreferencesStore.setDeveloperModeEnabled(event.enabled)
}
is AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled -> localCoroutineScope.launch {
sessionPreferencesStore.setSendPublicReadReceipts(event.enabled)
}
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
preferencesStore.setTheme(event.theme.name)
appPreferencesStore.setTheme(event.theme.name)
showChangeThemeDialog = false
}
}
@ -67,6 +75,7 @@ class AdvancedSettingsPresenter @Inject constructor(
return AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
theme = theme,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = { handleEvents(it) }

View file

@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
data class AdvancedSettingsState(
val isRichTextEditorEnabled: Boolean,
val isDeveloperModeEnabled: Boolean,
val isSendPublicReadReceiptsEnabled: Boolean,
val theme: Theme,
val showChangeThemeDialog: Boolean,
val eventSink: (AdvancedSettingsEvents) -> Unit

View file

@ -26,16 +26,19 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
aAdvancedSettingsState(isRichTextEditorEnabled = true),
aAdvancedSettingsState(isDeveloperModeEnabled = true),
aAdvancedSettingsState(showChangeThemeDialog = true),
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
)
}
fun aAdvancedSettingsState(
isRichTextEditorEnabled: Boolean = false,
isDeveloperModeEnabled: Boolean = false,
isSendPublicReadReceiptsEnabled: Boolean = false,
showChangeThemeDialog: Boolean = false,
) = AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
theme = Theme.System,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = {}

View file

@ -66,8 +66,8 @@ fun AdvancedSettingsView(
},
trailingContent = ListItemContent.Switch(
checked = state.isRichTextEditorEnabled,
onChange = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(it)) },
),
onClick = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(!state.isRichTextEditorEnabled)) }
)
ListItem(
headlineContent = {
@ -78,8 +78,20 @@ fun AdvancedSettingsView(
},
trailingContent = ListItemContent.Switch(
checked = state.isDeveloperModeEnabled,
onChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
),
onClick = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(!state.isDeveloperModeEnabled)) }
)
ListItem(
headlineContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts))
},
supportingContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts_description))
},
trailingContent = ListItemContent.Switch(
checked = state.isSendPublicReadReceiptsEnabled,
),
onClick = { state.eventSink(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(!state.isSendPublicReadReceiptsEnabled)) }
)
}

View file

@ -28,7 +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.preferences.api.store.PreferencesStore
import io.element.android.features.preferences.api.store.AppPreferencesStore
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
@ -51,7 +51,7 @@ class DeveloperSettingsPresenter @Inject constructor(
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
private val clearCacheUseCase: ClearCacheUseCase,
private val rageshakePresenter: RageshakePreferencesPresenter,
private val preferencesStore: PreferencesStore,
private val appPreferencesStore: AppPreferencesStore,
) : Presenter<DeveloperSettingsState> {
@Composable
override fun present(): DeveloperSettingsState {
@ -69,7 +69,7 @@ class DeveloperSettingsPresenter @Inject constructor(
val clearCacheAction = remember {
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
}
val customElementCallBaseUrl by preferencesStore
val customElementCallBaseUrl by appPreferencesStore
.getCustomElementCallBaseUrlFlow()
.collectAsState(initial = null)
@ -100,7 +100,7 @@ class DeveloperSettingsPresenter @Inject constructor(
is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch {
// If the URL is either empty or the default one, we want to save 'null' to remove the custom URL
val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL }
preferencesStore.setCustomElementCallBaseUrl(urlToSave)
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
}
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
}

View file

@ -21,7 +21,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
import kotlinx.collections.immutable.persistentListOf
open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> {
@ -49,14 +49,12 @@ private fun anEditDefaultNotificationSettingsState(
)
private fun aRoomSummary() = RoomSummary.Filled(
RoomSummaryDetails(
aRoomSummaryDetails(
roomId = RoomId("!roomId:domain"),
name = "Room",
avatarURLString = null,
avatarUrl = null,
isDirect = false,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
)
)

View file

@ -84,7 +84,7 @@ fun EditDefaultNotificationSettingView(
if (state.roomsWithUserDefinedMode.isNotEmpty()) {
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) {
state.roomsWithUserDefinedMode.forEach { summary ->
val subtitle = when (summary.details.notificationMode) {
val subtitle = when (summary.details.userDefinedNotificationMode) {
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages)
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> {
stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
@ -95,7 +95,7 @@ fun EditDefaultNotificationSettingView(
val avatarData = AvatarData(
id = summary.identifier(),
name = summary.details.name,
url = summary.details.avatarURLString,
url = summary.details.avatarUrl,
size = AvatarSize.CustomRoomNotificationSetting,
)
ListItem(

View file

@ -99,6 +99,7 @@ class PreferencesRootPresenter @Inject constructor(
return PreferencesRootState(
myUser = matrixUser.value,
version = versionFormatter.get(),
deviceId = matrixClient.deviceId,
showCompleteVerification = showCompleteVerification,
showSecureBackup = !showCompleteVerification && secureStorageFlag == true,
showSecureBackupBadge = showSecureBackupIndicator,

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
data class PreferencesRootState(
val myUser: MatrixUser?,
val version: String,
val deviceId: String?,
val showCompleteVerification: Boolean,
val showSecureBackup: Boolean,
val showSecureBackupBadge: Boolean,

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun aPreferencesRootState() = PreferencesRootState(
myUser = null,
version = "Version 1.1 (1)",
deviceId = "ILAKNDNASDLK",
showCompleteVerification = true,
showSecureBackup = true,
showSecureBackupBadge = true,

View file

@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.InsertChart
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -164,18 +165,38 @@ fun PreferencesRootView(
style = ListItemStyle.Destructive,
onClick = onSignOutClicked,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 40.dp, bottom = 24.dp),
textAlign = TextAlign.Center,
text = state.version,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.materialColors.secondary,
Footer(
version = state.version,
deviceId = state.deviceId,
)
}
}
@Composable
private fun Footer(
version: String,
deviceId: String?
) {
val text = remember(version, deviceId) {
buildString {
append(version)
if (deviceId != null) {
append("\n")
append(deviceId)
}
}
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 40.dp, bottom = 24.dp),
textAlign = TextAlign.Center,
text = text,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.materialColors.secondary,
)
}
@Composable
private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
ListItem(

View file

@ -33,16 +33,16 @@ class DefaultVersionFormatter @Inject constructor(
private val buildMeta: BuildMeta,
) : VersionFormatter {
override fun get(): String {
return stringProvider.getString(
val base = stringProvider.getString(
CommonStrings.settings_version_number,
buildMeta.versionName,
buildMeta.versionCode.toString()
)
}
}
class FakeVersionFormatter : VersionFormatter {
override fun get(): String {
return "A Version"
return if (buildMeta.gitBranchName == "main") {
base
} else {
// In case of a build not from main, we display the branch name and the revision
"$base\n${buildMeta.gitBranchName} (${buildMeta.gitRevision})"
}
}
}

View file

@ -6,6 +6,8 @@
<string name="screen_advanced_settings_developer_mode">"Vývojářský režim"</string>
<string name="screen_advanced_settings_developer_mode_description">"Povolením získáte přístup k funkcím a funkcím pro vývojáře."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Vypněte editor formátovaného textu pro ruční zadání Markdown."</string>
<string name="screen_advanced_settings_send_read_receipts">"Potvrzení o přečtení"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Pokud je vypnuto, potvrzení o přečtení se nikomu neodesílají. Stále budete dostávat potvrzení o přečtení od ostatních uživatelů."</string>
<string name="screen_advanced_settings_view_source_description">"Povolit možnost zobrazení zdroje zprávy na časové ose."</string>
<string name="screen_edit_profile_display_name">"Zobrazované jméno"</string>
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované jméno"</string>

View file

@ -6,6 +6,8 @@
<string name="screen_advanced_settings_developer_mode">"Mode développeur"</string>
<string name="screen_advanced_settings_developer_mode_description">"Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Désactivez léditeur de texte enrichi pour saisir manuellement du Markdown."</string>
<string name="screen_advanced_settings_send_read_receipts">"Accusés de lecture"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"En cas de désactivation, vos accusés de lecture ne seront pas envoyés aux autres membres. Vous verrez toujours les accusés des autres membres."</string>
<string name="screen_advanced_settings_view_source_description">"Activer cette option pour pouvoir voir la source des messages dans la discussion."</string>
<string name="screen_edit_profile_display_name">"Pseudonyme"</string>
<string name="screen_edit_profile_display_name_placeholder">"Votre pseudonyme"</string>

View file

@ -2,11 +2,11 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_element_call_base_url">"Egyéni Element Call alapwebcím"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Egyéni alapwebcím beállítása az Element Callhoz."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Érvénytelen webcím, győződjön meg arról, hogy szerepel benne a protokoll (http/https), és hogy helyes a cím."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Érvénytelen webcím, győződj meg arról, hogy szerepel-e benne a protokoll (http/https), és hogy helyes-e a cím."</string>
<string name="screen_advanced_settings_developer_mode">"Fejlesztői mód"</string>
<string name="screen_advanced_settings_developer_mode_description">"Engedélyezze, hogy elérje a fejlesztőknek szánt funkciókat."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"A formázott szöveges szerkesztő letiltása, hogy kézzel írhasson Markdownt."</string>
<string name="screen_advanced_settings_view_source_description">"Engedélyezze a beállítást az üzenet forrásának megjelenítéséhez az idővonalon."</string>
<string name="screen_advanced_settings_developer_mode_description">"Engedélyezd, hogy elérd a fejlesztőknek szánt funkciókat."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"A formázott szöveges szerkesztő letiltása, hogy kézzel írhass Markdownt."</string>
<string name="screen_advanced_settings_view_source_description">"Engedélyezd a beállítást az üzenet forrásának megjelenítéséhez az idővonalon."</string>
<string name="screen_edit_profile_display_name">"Megjelenítendő név"</string>
<string name="screen_edit_profile_display_name_placeholder">"Saját megjelenítendő név"</string>
<string name="screen_edit_profile_error">"Ismeretlen hiba történt, és az információ módosítása nem sikerült."</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_advanced_settings_element_call_base_url">"URL base di Element Call personalizzato"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Imposta un URL di base personalizzato per Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL non valido, assicurati di includere il protocollo (http/https) e l\'indirizzo corretto."</string>
<string name="screen_advanced_settings_developer_mode">"Modalità sviluppatore"</string>
<string name="screen_advanced_settings_developer_mode_description">"Attiva per avere accesso a caratteristiche e funzionalità per sviluppatori."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Disattiva l\'editor in rich text per digitare Markdown manualmente."</string>
<string name="screen_advanced_settings_view_source_description">"Attiva l\'opzione per visualizzare il sorgente del messaggio nella linea temporale."</string>
<string name="screen_edit_profile_display_name">"Nome da mostrare"</string>
<string name="screen_edit_profile_display_name_placeholder">"Il tuo nome da mostrare"</string>
<string name="screen_edit_profile_error">"Si è verificato un errore sconosciuto e non è stato possibile modificare le informazioni."</string>
<string name="screen_edit_profile_error_title">"Impossibile aggiornare il profilo"</string>
<string name="screen_edit_profile_title">"Modifica profilo"</string>
<string name="screen_edit_profile_updating_details">"Aggiornamento del profilo…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Impostazioni aggiuntive"</string>
<string name="screen_notification_settings_calls_label">"Chiamate audio e video"</string>
<string name="screen_notification_settings_configuration_mismatch">"Mancata corrispondenza della configurazione"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Abbiamo semplificato le impostazioni di notifica per rendere le opzioni più facili da trovare. Alcune impostazioni personalizzate che hai scelto in passato non sono mostrate qui, ma sono ancora attive.
Se procedi, alcune delle tue impostazioni potrebbero cambiare."</string>
<string name="screen_notification_settings_direct_chats">"Chat dirette"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Impostazione personalizzata per chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Si è verificato un errore durante l\'aggiornamento delle impostazioni di notifica."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Tutti i messaggi"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Solo menzioni e parole chiave"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Nelle chat dirette, avvisami per"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Nelle chat di gruppo, avvisami per"</string>
<string name="screen_notification_settings_enable_notifications">"Attiva le notifiche su questo dispositivo"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"La configurazione non è stata corretta, riprova."</string>
<string name="screen_notification_settings_group_chats">"Chat di gruppo"</string>
<string name="screen_notification_settings_invite_for_me_label">"Inviti"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Il tuo homeserver non supporta questa opzione nelle stanze criptate, quindi potresti non ricevere notifiche in alcune stanze."</string>
<string name="screen_notification_settings_mentions_section_title">"Menzioni"</string>
<string name="screen_notification_settings_mode_all">"Tutto"</string>
<string name="screen_notification_settings_mode_mentions">"Menzioni"</string>
<string name="screen_notification_settings_notification_section_title">"Avvisami per"</string>
<string name="screen_notification_settings_room_mention_label">"Avvisami su @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Per ricevere notifiche, modifica le tue %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"impostazioni di sistema"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Notifiche di sistema disattivate"</string>
<string name="screen_notification_settings_title">"Notifiche"</string>
</resources>

View file

@ -6,7 +6,9 @@
<string name="screen_advanced_settings_developer_mode">"Режим разработчика"</string>
<string name="screen_advanced_settings_developer_mode_description">"Предоставьте разработчикам доступ к функциям и функциональным возможностям."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Отключить редактор форматированного текста и включить Markdown."</string>
<string name="screen_advanced_settings_view_source_description">"Включить опцию просмотра источника сообщения на временной шкале."</string>
<string name="screen_advanced_settings_send_read_receipts">"Уведомления о прочтении"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Если этот параметр выключен, ваш статус о прочтении не будет отображаться. Вы по-прежнему будете видеть статус о прочтении от других пользователей."</string>
<string name="screen_advanced_settings_view_source_description">"Включить опцию просмотра источника сообщения в ленте."</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>
@ -20,7 +22,7 @@
Если вы продолжите, некоторые настройки могут быть изменены."</string>
<string name="screen_notification_settings_direct_chats">"Прямые чаты"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Индивидуальные настройки для каждого чата"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Персональные настройки для каждого чата"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"При обновлении настроек уведомления произошла ошибка."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Все сообщения"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Только упоминания и ключевые слова"</string>

View file

@ -6,6 +6,8 @@
<string name="screen_advanced_settings_developer_mode">"Vývojársky režim"</string>
<string name="screen_advanced_settings_developer_mode_description">"Umožniť prístup k možnostiam a funkciám pre vývojárov."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Vypnite rozšírený textový editor na ručné písanie Markdown."</string>
<string name="screen_advanced_settings_send_read_receipts">"Potvrdenia o prečítaní"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Ak je táto funkcia vypnutá, vaše potvrdenia o prečítaní sa nebudú nikomu odosielať. Stále budete dostávať potvrdenia o prečítaní od ostatných používateľov."</string>
<string name="screen_advanced_settings_view_source_description">"Povoliť možnosť zobrazenia zdroja správy na časovej osi."</string>
<string name="screen_edit_profile_display_name">"Zobrazované meno"</string>
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované meno"</string>

View file

@ -6,6 +6,8 @@
<string name="screen_advanced_settings_developer_mode">"Developer mode"</string>
<string name="screen_advanced_settings_developer_mode_description">"Enable to have access to features and functionality for developers."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
<string name="screen_advanced_settings_send_read_receipts">"Read receipts"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users."</string>
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
<string name="screen_edit_profile_display_name">"Display name"</string>
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>

View file

@ -21,7 +21,8 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.compound.theme.Theme
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.test.runTest
@ -34,8 +35,7 @@ class AdvancedSettingsPresenterTest {
@Test
fun `present - initial state`() = runTest {
val store = InMemoryPreferencesStore()
val presenter = AdvancedSettingsPresenter(store)
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -43,14 +43,14 @@ class AdvancedSettingsPresenterTest {
assertThat(initialState.isDeveloperModeEnabled).isFalse()
assertThat(initialState.isRichTextEditorEnabled).isFalse()
assertThat(initialState.showChangeThemeDialog).isFalse()
assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue()
assertThat(initialState.theme).isEqualTo(Theme.System)
}
}
@Test
fun `present - developer mode on off`() = runTest {
val store = InMemoryPreferencesStore()
val presenter = AdvancedSettingsPresenter(store)
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -65,8 +65,7 @@ class AdvancedSettingsPresenterTest {
@Test
fun `present - rich text editor on off`() = runTest {
val store = InMemoryPreferencesStore()
val presenter = AdvancedSettingsPresenter(store)
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -79,10 +78,24 @@ class AdvancedSettingsPresenterTest {
}
}
@Test
fun `present - send public read receipts off on`() = runTest {
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(false))
assertThat(awaitItem().isSendPublicReadReceiptsEnabled).isFalse()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(true))
assertThat(awaitItem().isSendPublicReadReceiptsEnabled).isTrue()
}
}
@Test
fun `present - change theme`() = runTest {
val store = InMemoryPreferencesStore()
val presenter = AdvancedSettingsPresenter(store)
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -102,4 +115,12 @@ class AdvancedSettingsPresenterTest {
assertThat(withNewTheme.theme).isEqualTo(Theme.Light)
}
}
private fun createAdvancedSettingsPresenter(
appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
) = AdvancedSettingsPresenter(
appPreferencesStore = appPreferencesStore,
sessionPreferencesStore = sessionPreferencesStore,
)
}

View file

@ -29,7 +29,7 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataSto
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.test.runTest
@ -114,7 +114,7 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - custom element call base url`() = runTest {
val preferencesStore = InMemoryPreferencesStore()
val preferencesStore = InMemoryAppPreferencesStore()
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -149,14 +149,14 @@ class DeveloperSettingsPresenterTest {
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
preferencesStore: InMemoryPreferencesStore = InMemoryPreferencesStore(),
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
): DeveloperSettingsPresenter {
return DeveloperSettingsPresenter(
featureFlagService = featureFlagService,
computeCacheSizeUseCase = cacheSizeUseCase,
clearCacheUseCase = clearCacheUseCase,
rageshakePresenter = rageshakePresenter,
preferencesStore = preferencesStore,
appPreferencesStore = preferencesStore,
)
}
}

View file

@ -29,7 +29,7 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
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.aRoomSummaryDetail
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate
@ -72,11 +72,11 @@ class EditDefaultNotificationSettingsPresenterTests {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
roomListService.postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail(notificationMode = RoomNotificationMode.ALL_MESSAGES))))
roomListService.postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetails(notificationMode = RoomNotificationMode.ALL_MESSAGES))))
val loadedState = consumeItemsUntilPredicate { state ->
state.roomsWithUserDefinedMode.any { it.details.notificationMode == RoomNotificationMode.ALL_MESSAGES }
state.roomsWithUserDefinedMode.any { it.details.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES }
}.last()
assertThat(loadedState.roomsWithUserDefinedMode.any { it.details.notificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue()
assertThat(loadedState.roomsWithUserDefinedMode.any { it.details.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue()
}
}

View file

@ -0,0 +1,23 @@
/*
* 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.preferences.impl.root
class FakeVersionFormatter : VersionFormatter {
override fun get(): String {
return "A Version"
}
}

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