Merge branch 'develop' into feature/fga/room_list_filters

This commit is contained in:
ganfra 2024-02-16 11:30:20 +01:00
commit 9b2198f12b
1489 changed files with 5726 additions and 4385 deletions

View file

@ -41,7 +41,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RoomListContextMenu(
contextMenu: RoomListState.ContextMenu.Shown,
eventSink: (RoomListEvents) -> Unit,
eventSink: (RoomListEvents.ContextMenuEvents) -> Unit,
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
) {
ModalBottomSheet(
@ -49,14 +49,25 @@ fun RoomListContextMenu(
) {
RoomListModalBottomSheetContent(
contextMenu = contextMenu,
onRoomMarkReadClicked = {
eventSink(RoomListEvents.HideContextMenu)
eventSink(RoomListEvents.MarkAsRead(contextMenu.roomId))
},
onRoomMarkUnreadClicked = {
eventSink(RoomListEvents.HideContextMenu)
eventSink(RoomListEvents.MarkAsUnread(contextMenu.roomId))
},
onRoomSettingsClicked = {
eventSink(RoomListEvents.HideContextMenu)
onRoomSettingsClicked(it)
onRoomSettingsClicked(contextMenu.roomId)
},
onLeaveRoomClicked = {
eventSink(RoomListEvents.HideContextMenu)
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId))
}
},
onFavoriteChanged = { isFavorite ->
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
},
)
}
}
@ -64,8 +75,11 @@ fun RoomListContextMenu(
@Composable
private fun RoomListModalBottomSheetContent(
contextMenu: RoomListState.ContextMenu.Shown,
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
onLeaveRoomClicked: (roomId: RoomId) -> Unit,
onRoomSettingsClicked: () -> Unit,
onLeaveRoomClicked: () -> Unit,
onFavoriteChanged: (isFavorite: Boolean) -> Unit,
onRoomMarkReadClicked: () -> Unit,
onRoomMarkUnreadClicked: () -> Unit,
) {
Column(
modifier = Modifier.fillMaxWidth()
@ -78,6 +92,62 @@ private fun RoomListModalBottomSheetContent(
)
}
)
if (contextMenu.markAsUnreadFeatureFlagEnabled) {
ListItem(
headlineContent = {
Text(
text = stringResource(
id = if (contextMenu.hasNewContent) {
R.string.screen_roomlist_mark_as_read
} else {
R.string.screen_roomlist_mark_as_unread
}
),
style = MaterialTheme.typography.bodyLarge,
)
},
modifier = Modifier.clickable {
if (contextMenu.hasNewContent) {
onRoomMarkReadClicked()
} else {
onRoomMarkUnreadClicked()
}
},
/* TODO Design
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Settings,
contentDescription = stringResource(id = CommonStrings.common_settings)
)
),
*/
style = ListItemStyle.Primary,
)
}
ListItem(
headlineContent = {
Text(
text = stringResource(id = CommonStrings.common_favourite),
style = MaterialTheme.typography.bodyLarge,
)
},
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Favourite(),
contentDescription = stringResource(id = CommonStrings.common_favourite),
)
),
trailingContent = ListItemContent.Switch(
checked = contextMenu.isFavorite,
onChange = { isFavorite ->
onFavoriteChanged(isFavorite)
},
),
onClick = {
onFavoriteChanged(!contextMenu.isFavorite)
},
style = ListItemStyle.Primary,
)
ListItem(
headlineContent = {
Text(
@ -85,10 +155,10 @@ private fun RoomListModalBottomSheetContent(
style = MaterialTheme.typography.bodyLarge,
)
},
modifier = Modifier.clickable { onRoomSettingsClicked(contextMenu.roomId) },
modifier = Modifier.clickable { onRoomSettingsClicked() },
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Settings,
CompoundIcons.Settings(),
contentDescription = stringResource(id = CommonStrings.common_settings)
)
),
@ -96,17 +166,19 @@ private fun RoomListModalBottomSheetContent(
)
ListItem(
headlineContent = {
val leaveText = stringResource(id = if (contextMenu.isDm) {
CommonStrings.action_leave_conversation
} else {
CommonStrings.action_leave_room
})
val leaveText = stringResource(
id = if (contextMenu.isDm) {
CommonStrings.action_leave_conversation
} else {
CommonStrings.action_leave_room
}
)
Text(text = leaveText)
},
modifier = Modifier.clickable { onLeaveRoomClicked(contextMenu.roomId) },
modifier = Modifier.clickable { onLeaveRoomClicked() },
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Leave,
CompoundIcons.Leave(),
contentDescription = stringResource(id = CommonStrings.action_leave_room)
)
),
@ -122,13 +194,12 @@ private fun RoomListModalBottomSheetContent(
@Composable
internal fun RoomListModalBottomSheetContentPreview() = ElementPreview {
RoomListModalBottomSheetContent(
contextMenu = RoomListState.ContextMenu.Shown(
roomId = RoomId(value = "!aRoom:aDomain"),
roomName = "aRoom",
isDm = false,
),
contextMenu = aContextMenuShown(hasNewContent = true),
onRoomMarkReadClicked = {},
onRoomMarkUnreadClicked = {},
onRoomSettingsClicked = {},
onLeaveRoomClicked = {}
onLeaveRoomClicked = {},
onFavoriteChanged = {},
)
}
@ -136,12 +207,11 @@ internal fun RoomListModalBottomSheetContentPreview() = ElementPreview {
@Composable
internal fun RoomListModalBottomSheetContentForDmPreview() = ElementPreview {
RoomListModalBottomSheetContent(
contextMenu = RoomListState.ContextMenu.Shown(
roomId = RoomId(value = "!aRoom:aDomain"),
roomName = "aRoom",
isDm = true,
),
contextMenu = aContextMenuShown(isDm = true),
onRoomMarkReadClicked = {},
onRoomMarkUnreadClicked = {},
onRoomSettingsClicked = {},
onLeaveRoomClicked = {}
onLeaveRoomClicked = {},
onFavoriteChanged = {},
)
}

View file

@ -26,6 +26,11 @@ sealed interface RoomListEvents {
data object DismissRecoveryKeyPrompt : RoomListEvents
data object ToggleSearchResults : RoomListEvents
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
data object HideContextMenu : RoomListEvents
data class LeaveRoom(val roomId: RoomId) : RoomListEvents
sealed interface ContextMenuEvents : RoomListEvents
data object HideContextMenu : ContextMenuEvents
data class LeaveRoom(val roomId: RoomId) : ContextMenuEvents
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
}

View file

@ -25,15 +25,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@ -44,10 +48,19 @@ import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.getCurrentUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -65,9 +78,12 @@ class RoomListPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService,
private val filtersPresenter: RoomListFiltersPresenter,
private val migrationScreenPresenter: MigrationScreenPresenter,
private val sessionPreferencesStore: SessionPreferencesStore,
) : Presenter<RoomListState> {
@Composable
override fun present(): RoomListState {
val coroutineScope = rememberCoroutineScope()
val leaveRoomState = leaveRoomPresenter.present()
val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
mutableStateOf(null)
@ -85,6 +101,8 @@ class RoomListPresenter @Inject constructor(
initialLoad(matrixUser)
}
val isMigrating = migrationScreenPresenter.present().isMigrating
// Session verification status (unknown, not verified, verified)
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
@ -108,8 +126,7 @@ class RoomListPresenter @Inject constructor(
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
var contextMenu by remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
fun handleEvents(event: RoomListEvents) {
when (event) {
@ -124,14 +141,34 @@ class RoomListPresenter @Inject constructor(
displaySearchResults = !displaySearchResults
}
is RoomListEvents.ShowContextMenu -> {
contextMenu = RoomListState.ContextMenu.Shown(
roomId = event.roomListRoomSummary.roomId,
roomName = event.roomListRoomSummary.name,
isDm = event.roomListRoomSummary.isDm,
)
coroutineScope.showContextMenu(event, contextMenu)
}
is RoomListEvents.HideContextMenu -> {
contextMenu.value = RoomListState.ContextMenu.Hidden
}
is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId))
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.launch {
client.getRoom(event.roomId)?.use { room ->
room.setIsFavorite(event.isFavorite)
}
}
is RoomListEvents.MarkAsRead -> coroutineScope.launch {
client.getRoom(event.roomId)?.use { room ->
room.setUnreadFlag(isUnread = false)
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
ReceiptType.READ
} else {
ReceiptType.READ_PRIVATE
}
room.markAsRead(receiptType)
}
}
is RoomListEvents.MarkAsUnread -> coroutineScope.launch {
client.getRoom(event.roomId)?.use { room ->
room.setUnreadFlag(isUnread = true)
}
}
}
}
@ -149,10 +186,11 @@ class RoomListPresenter @Inject constructor(
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
invitesState = inviteStateDataSource.inviteState(),
displaySearchResults = displaySearchResults,
contextMenu = contextMenu,
contextMenu = contextMenu.value,
leaveRoomState = leaveRoomState,
filtersState = filtersState,
eventSink = ::handleEvents
displayMigrationStatus = isMigrating,
eventSink = ::handleEvents,
)
}
@ -160,6 +198,37 @@ class RoomListPresenter @Inject constructor(
matrixUser.value = client.getCurrentUser()
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState<RoomListState.ContextMenu>) = launch {
val initialState = RoomListState.ContextMenu.Shown(
roomId = event.roomListRoomSummary.roomId,
roomName = event.roomListRoomSummary.name,
isDm = event.roomListRoomSummary.isDm,
isFavorite = event.roomListRoomSummary.isFavorite,
markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread),
hasNewContent = event.roomListRoomSummary.hasNewContent
)
contextMenuState.value = initialState
client.getRoom(event.roomListRoomSummary.roomId)?.use { room ->
val isShowingContextMenuFlow = snapshotFlow { contextMenuState.value is RoomListState.ContextMenu.Shown }
.distinctUntilChanged()
val isFavoriteFlow = room.roomInfoFlow
.map { it.isFavorite }
.distinctUntilChanged()
isFavoriteFlow
.onEach { isFavorite ->
contextMenuState.value = initialState.copy(isFavorite = isFavorite)
}
.flatMapLatest { isShowingContextMenuFlow }
.takeWhile { isShowingContextMenu -> isShowingContextMenu }
.collect()
}
}
private fun updateVisibleRange(range: IntRange) {
if (range.isEmpty()) return
val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2

View file

@ -42,6 +42,7 @@ data class RoomListState(
val contextMenu: ContextMenu,
val leaveRoomState: LeaveRoomState,
val filtersState: RoomListFiltersState,
val displayMigrationStatus: Boolean,
val eventSink: (RoomListEvents) -> Unit,
) {
sealed interface ContextMenu {
@ -50,6 +51,9 @@ data class RoomListState(
val roomId: RoomId,
val roomName: String,
val isDm: Boolean,
val isFavorite: Boolean,
val markAsUnreadFeatureFlagEnabled: Boolean,
val hasNewContent: Boolean,
) : ContextMenu
}
}

View file

@ -44,16 +44,12 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState().copy(invitesState = InvitesState.NewInvites),
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
aRoomListState().copy(displaySearchResults = true),
aRoomListState().copy(
contextMenu = RoomListState.ContextMenu.Shown(
roomId = RoomId("!aRoom:aDomain"),
roomName = "A nice room name",
isDm = false,
)
),
aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")),
aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)),
aRoomListState().copy(displayRecoveryKeyPrompt = true),
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
aRoomListState().copy(matrixUser = null, displayMigrationStatus = true),
)
}
@ -72,6 +68,7 @@ internal fun aRoomListState() = RoomListState(
contextMenu = RoomListState.ContextMenu.Hidden,
leaveRoomState = aLeaveRoomState(),
filtersState = aRoomListFiltersState(),
displayMigrationStatus = false,
eventSink = {}
)
@ -103,3 +100,17 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
),
)
}
internal fun aContextMenuShown(
roomName: String = "aRoom",
isDm: Boolean = false,
hasNewContent: Boolean = false,
isFavorite: Boolean = false,
) = RoomListState.ContextMenu.Shown(
roomId = RoomId("!aRoom:aDomain"),
roomName = roomName,
isDm = isDm,
markAsUnreadFeatureFlagEnabled = true,
hasNewContent = hasNewContent,
isFavorite = isFavorite,
)

View file

@ -47,6 +47,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
import io.element.android.features.roomlist.impl.components.ConfirmRecoveryKeyBanner
@ -55,6 +56,7 @@ import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.features.roomlist.impl.components.RoomListTopBar
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
import io.element.android.features.roomlist.impl.filters.RoomListFiltersView
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
import io.element.android.libraries.architecture.AsyncData
@ -67,8 +69,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId
@ -156,7 +156,7 @@ private fun EmptyRoomListView(
Spacer(modifier = Modifier.height(16.dp))
Button(
text = stringResource(CommonStrings.action_start_chat),
leadingIcon = IconSource.Resource(CommonDrawables.ic_new_message),
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
onClick = onCreateRoomClicked,
)
}
@ -191,11 +191,6 @@ private fun RoomListContent(
}
}
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
LogCompositions(
tag = "RoomListScreen",
msg = "Content"
)
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
@ -220,6 +215,7 @@ private fun RoomListContent(
onMenuActionClicked = onMenuActionClicked,
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
displayMenuItems = !state.displayMigrationStatus,
)
RoomListFiltersView(state = state.filtersState)
}
@ -280,18 +276,22 @@ private fun RoomListContent(
}
}
}
MigrationScreenView(isMigrating = state.displayMigrationStatus)
},
floatingActionButton = {
FloatingActionButton(
// FIXME align on Design system theme
containerColor = MaterialTheme.colorScheme.primary,
onClick = onCreateRoomClicked
) {
Icon(
// Note cannot use Icons.Outlined.EditSquare, it does not exist :/
resourceId = CommonDrawables.ic_new_message,
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message)
)
if (!state.displayMigrationStatus) {
FloatingActionButton(
// FIXME align on Design system theme
containerColor = MaterialTheme.colorScheme.primary,
onClick = onCreateRoomClicked
) {
Icon(
// Note cannot use Icons.Outlined.EditSquare, it does not exist :/
imageVector = CompoundIcons.Compose(),
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message)
)
}
}
},
snackbarHost = { SnackbarHost(snackbarHostState) },

View file

@ -21,8 +21,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults
@ -68,8 +70,8 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
@ -90,13 +92,9 @@ fun RoomListTopBar(
onMenuActionClicked: (RoomListMenuAction) -> Unit,
onOpenSettings: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
displayMenuItems: Boolean,
modifier: Modifier = Modifier,
) {
LogCompositions(
tag = "RoomListScreen",
msg = "TopBar"
)
fun closeFilter() {
onFilterChanged("")
}
@ -114,6 +112,7 @@ fun RoomListTopBar(
onSearchClicked = onToggleSearch,
onMenuActionClicked = onMenuActionClicked,
scrollBehavior = scrollBehavior,
displayMenuItems = displayMenuItems,
modifier = modifier,
)
}
@ -128,6 +127,7 @@ private fun DefaultRoomListTopBar(
onOpenSettings: () -> Unit,
onSearchClicked: () -> Unit,
onMenuActionClicked: (RoomListMenuAction) -> Unit,
displayMenuItems: Boolean,
modifier: Modifier = Modifier,
) {
// We need this to manually clip the top app bar in preview mode
@ -201,79 +201,89 @@ private fun DefaultRoomListTopBar(
Text(text = stringResource(id = R.string.screen_roomlist_main_space_title))
},
navigationIcon = {
avatarData?.let {
IconButton(
modifier = Modifier.testTag(TestTags.homeScreenSettings),
onClick = onOpenSettings
) {
IconButton(
modifier = Modifier.testTag(TestTags.homeScreenSettings),
onClick = onOpenSettings
) {
if (avatarData != null) {
Avatar(
avatarData = it,
avatarData = avatarData!!,
contentDescription = stringResource(CommonStrings.common_settings),
)
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier
.padding(4.5.dp)
.align(Alignment.TopEnd)
)
}
} else {
// Placeholder avatar until the avatarData is available
Surface(
modifier = Modifier.size(AvatarSize.CurrentUserTopBar.dp),
shape = CircleShape,
color = ElementTheme.colors.iconSecondary,
content = {}
)
}
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier
.padding(4.5.dp)
.align(Alignment.TopEnd)
)
}
}
},
actions = {
IconButton(
onClick = onSearchClicked,
) {
Icon(
imageVector = CompoundIcons.Search,
contentDescription = stringResource(CommonStrings.action_search),
)
}
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
var showMenu by remember { mutableStateOf(false) }
if (displayMenuItems) {
IconButton(
onClick = { showMenu = !showMenu }
onClick = onSearchClicked,
) {
Icon(
imageVector = CompoundIcons.OverflowVertical,
contentDescription = null,
imageVector = CompoundIcons.Search(),
contentDescription = stringResource(CommonStrings.action_search),
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClicked(RoomListMenuAction.InviteFriends)
},
text = { Text(stringResource(id = CommonStrings.action_invite)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ShareAndroid,
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
}
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
var showMenu by remember { mutableStateOf(false) }
IconButton(
onClick = { showMenu = !showMenu }
) {
Icon(
imageVector = CompoundIcons.OverflowVertical(),
contentDescription = null,
)
}
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClicked(RoomListMenuAction.ReportBug)
},
text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ChatProblem,
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
}
)
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClicked(RoomListMenuAction.InviteFriends)
},
text = { Text(stringResource(id = CommonStrings.action_invite)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
}
)
}
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClicked(RoomListMenuAction.ReportBug)
},
text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ChatProblem(),
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
}
)
}
}
}
}
@ -305,6 +315,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onSearchClicked = {},
displayMenuItems = true,
onMenuActionClicked = {},
)
}
@ -320,6 +331,7 @@ internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onSearchClicked = {},
displayMenuItems = true,
onMenuActionClicked = {},
)
}

View file

@ -197,7 +197,7 @@ private fun OnGoingCallIcon(
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.VideoCallSolid,
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = null,
tint = color,
)
@ -208,7 +208,7 @@ private fun NotificationOffIndicatorAtom() {
Icon(
modifier = Modifier.size(16.dp),
contentDescription = null,
imageVector = CompoundIcons.NotificationsSolidOff,
imageVector = CompoundIcons.NotificationsOffSolid(),
tint = ElementTheme.colors.iconQuaternary,
)
}
@ -218,7 +218,7 @@ private fun MentionIndicatorAtom() {
Icon(
modifier = Modifier.size(16.dp),
contentDescription = null,
imageVector = CompoundIcons.Mention,
imageVector = CompoundIcons.Mention(),
tint = ElementTheme.colors.unreadIndicator,
)
}

View file

@ -45,9 +45,11 @@ class RoomListRoomSummaryFactory @Inject constructor(
numberOfUnreadMessages = 0,
numberOfUnreadMentions = 0,
numberOfUnreadNotifications = 0,
isMarkedUnread = false,
userDefinedNotificationMode = null,
hasRoomCall = false,
isDm = false,
isFavorite = false,
)
}
@ -73,6 +75,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
numberOfUnreadMessages = roomSummary.details.numUnreadMessages,
numberOfUnreadMentions = roomSummary.details.numUnreadMentions,
numberOfUnreadNotifications = roomSummary.details.numUnreadNotifications,
isMarkedUnread = roomSummary.details.isMarkedUnread,
timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp),
lastMessage = roomSummary.details.lastMessage?.let { message ->
roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
@ -82,6 +85,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
userDefinedNotificationMode = roomSummary.details.userDefinedNotificationMode,
hasRoomCall = roomSummary.details.hasRoomCall,
isDm = roomSummary.details.isDm,
isFavorite = roomSummary.details.isFavorite,
)
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.roomlist.impl.migration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import javax.inject.Inject
class MigrationScreenPresenter @Inject constructor(
private val matrixClient: MatrixClient,
private val migrationScreenStore: MigrationScreenStore,
) : Presenter<MigrationScreenState> {
@Composable
override fun present(): MigrationScreenState {
val roomListState by matrixClient.roomListService.state.collectAsState()
var needsMigration by remember { mutableStateOf(migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)) }
if (roomListState == RoomListService.State.Running) {
LaunchedEffect(Unit) {
needsMigration = false
migrationScreenStore.setMigrationScreenShown(matrixClient.sessionId)
}
}
return MigrationScreenState(
isMigrating = needsMigration && roomListState != RoomListService.State.Running
)
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2023 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.roomlist.impl.migration
data class MigrationScreenState(
val isMigrating: Boolean
)

View file

@ -0,0 +1,55 @@
/*
* 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.roomlist.impl.migration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@Composable
fun MigrationScreenView(
isMigrating: Boolean,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = isMigrating,
enter = fadeIn(),
exit = fadeOut(),
label = "Migration view fade",
) {
SunsetPage(
modifier = modifier,
isLoading = true,
title = stringResource(id = R.string.screen_migration_title),
subtitle = stringResource(id = R.string.screen_migration_message),
overallContent = {}
)
}
}
@Composable
@PreviewsDayNight
internal fun MigrationViewPreview() = ElementPreview {
MigrationScreenView(isMigrating = true)
}

View file

@ -0,0 +1,60 @@
/*
* 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.roomlist.impl.migration
import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.DefaultPreferences
import io.element.android.libraries.matrix.api.core.SessionId
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class SharedPrefsMigrationScreenStore @Inject constructor(
@DefaultPreferences private val sharedPreferences: SharedPreferences,
) : MigrationScreenStore {
override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean {
return sharedPreferences.getBoolean(sessionId.toKey(), false).not()
}
override fun setMigrationScreenShown(sessionId: SessionId) {
sharedPreferences.edit().putBoolean(sessionId.toKey(), true).apply()
}
override fun reset() {
sharedPreferences.edit {
sharedPreferences.all.keys
.filter { it.startsWith(IS_MIGRATION_SCREEN_SHOWN_PREFIX) }
.forEach {
remove(it)
}
}
}
private fun SessionId.toKey(): String {
// Hash the sessionId to get rid of exotic char and take only the first 16 chars,
// The risk of collision is not high.
return IS_MIGRATION_SCREEN_SHOWN_PREFIX + value.hash().take(16)
}
companion object {
private const val IS_MIGRATION_SCREEN_SHOWN_PREFIX = "is_migration_screen_shown_"
}
}

View file

@ -29,6 +29,7 @@ data class RoomListRoomSummary(
val numberOfUnreadMessages: Int,
val numberOfUnreadMentions: Int,
val numberOfUnreadNotifications: Int,
val isMarkedUnread: Boolean,
val timestamp: String?,
val lastMessage: CharSequence?,
val avatarData: AvatarData,
@ -36,11 +37,14 @@ data class RoomListRoomSummary(
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val isDm: Boolean,
val isFavorite: Boolean,
) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0)
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
isMarkedUnread
val hasNewContent = numberOfUnreadMessages > 0 ||
numberOfUnreadMentions > 0 ||
numberOfUnreadNotifications > 0
numberOfUnreadNotifications > 0 ||
isMarkedUnread
}

View file

@ -89,6 +89,7 @@ internal fun aRoomListRoomSummary(
numberOfUnreadMessages: Int = 0,
numberOfUnreadMentions: Int = 0,
numberOfUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
lastMessage: String? = "Last message",
timestamp: String? = lastMessage?.let { "88:88" },
isPlaceholder: Boolean = false,
@ -96,6 +97,7 @@ internal fun aRoomListRoomSummary(
hasRoomCall: Boolean = false,
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
isDm: Boolean = false,
isFavorite: Boolean = false,
) = RoomListRoomSummary(
id = id,
roomId = RoomId(id),
@ -103,6 +105,7 @@ internal fun aRoomListRoomSummary(
numberOfUnreadMessages = numberOfUnreadMessages,
numberOfUnreadMentions = numberOfUnreadMentions,
numberOfUnreadNotifications = numberOfUnreadNotifications,
isMarkedUnread = isMarkedUnread,
timestamp = timestamp,
lastMessage = lastMessage,
avatarData = avatarData,
@ -110,4 +113,5 @@ internal fun aRoomListRoomSummary(
userDefinedNotificationMode = notificationMode,
hasRoomCall = hasRoomCall,
isDm = isDm,
isFavorite = isFavorite,
)

View file

@ -103,7 +103,6 @@ private fun RoomListSearchResultContent(
state: RoomListState,
onRoomClicked: (RoomId) -> Unit,
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
modifier: Modifier = Modifier,
) {
val borderColor = MaterialTheme.colorScheme.tertiary
val strokeWidth = 1.dp
@ -115,7 +114,6 @@ private fun RoomListSearchResultContent(
onRoomClicked(room.roomId)
}
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
modifier = Modifier.drawBehind {
@ -152,7 +150,7 @@ private fun RoomListSearchResultContent(
state.eventSink(RoomListEvents.UpdateFilter(""))
}) {
Icon(
imageVector = CompoundIcons.Close,
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_cancel)
)
}

View file

@ -2,10 +2,13 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата."</string>
<string name="confirm_recovery_key_banner_title">"Пацвердзіце ключ аднаўлення"</string>
<string name="screen_migration_message">"Гэта аднаразовы працэс, дзякуем за чаканне."</string>
<string name="screen_migration_title">"Налада ўліковага запісу."</string>
<string name="screen_roomlist_a11y_create_message">"Стварыце новую размову або пакой"</string>
<string name="screen_roomlist_empty_message">"Пачніце з паведамлення каму-небудзь."</string>
<string name="screen_roomlist_empty_title">"Пакуль няма чатаў."</string>
<string name="screen_roomlist_main_space_title">"Усе чаты"</string>
<string name="session_verification_banner_message">"Здаецца, вы карыстаецеся новай прыладай. Праверце з дапамогай іншай прылады, каб атрымаць доступ да зашыфраваных паведамленняў."</string>
<string name="session_verification_banner_title">"Пацвердзіце, што гэта вы"</string>
<string name="screen_roomlist_filter_people">"Людзі"</string>
</resources>

View file

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Vaše záloha chatu není aktuálně synchronizována. Abyste si zachovali přístup k záloze chatu, musíte potvrdit klíč pro obnovení."</string>
<string name="confirm_recovery_key_banner_title">"Potvrďte klíč pro obnovení"</string>
<string name="screen_migration_message">"Jedná se o jednorázový proces, prosíme o strpení."</string>
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
<string name="screen_roomlist_a11y_create_message">"Vytvořte novou konverzaci nebo místnost"</string>
<string name="screen_roomlist_empty_message">"Začněte tím, že někomu pošnete zprávu."</string>
<string name="screen_roomlist_empty_title">"Zatím žádné konverzace."</string>

View file

@ -2,10 +2,18 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Dein Chat-Backup ist derzeit nicht synchronisiert. Du musst deinen Wiederherstellungsschlüssel bestätigen, um Zugriff auf dein Chat-Backup zu erhalten."</string>
<string name="confirm_recovery_key_banner_title">"Wiederherstellungsschlüssel bestätigen."</string>
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
<string name="screen_migration_title">"Dein Konto wird eingerichtet."</string>
<string name="screen_roomlist_a11y_create_message">"Eine neue Unterhaltung oder einen neuen Raum erstellen"</string>
<string name="screen_roomlist_empty_message">"Beginne, indem du jemandem eine Nachricht sendest."</string>
<string name="screen_roomlist_empty_title">"Noch keine Chats."</string>
<string name="screen_roomlist_filter_favourites">"Favoriten"</string>
<string name="screen_roomlist_filter_low_priority">"Niedrige Priorität"</string>
<string name="screen_roomlist_filter_rooms">"Räume"</string>
<string name="screen_roomlist_filter_unreads">"Ungelesen"</string>
<string name="screen_roomlist_main_space_title">"Alle Chats"</string>
<string name="screen_roomlist_mark_as_read">"Als gelesen markieren"</string>
<string name="screen_roomlist_mark_as_unread">"Als ungelesen markieren"</string>
<string name="session_verification_banner_message">"Es sieht aus, als würdest du ein neues Gerät verwenden. Verifiziere es mit einem anderen Gerät, damit du auf deine verschlüsselten Nachrichten zugreifen kannst."</string>
<string name="session_verification_banner_title">"Bestätige deine Identität"</string>
<string name="screen_roomlist_filter_people">"Personen"</string>

View file

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"La copia de seguridad del chat no está sincronizada en este momento. Debes confirmar tu clave de recuperación para mantener el acceso a la copia de seguridad del chat."</string>
<string name="confirm_recovery_key_banner_title">"Confirma tu clave de recuperación"</string>
<string name="screen_migration_message">"Este proceso solo se hace una vez, gracias por esperar."</string>
<string name="screen_migration_title">"Configura tu cuenta"</string>
<string name="screen_roomlist_a11y_create_message">"Crear una nueva conversación o sala"</string>
<string name="screen_roomlist_empty_message">"Empieza enviando un mensaje a alguien."</string>
<string name="screen_roomlist_empty_title">"Aún no hay chats."</string>

View file

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"La sauvegarde des conversations est désynchronisée. Vous devez confirmer la clé de récupération pour accéder à votre historique."</string>
<string name="confirm_recovery_key_banner_title">"Confirmer votre clé de récupération"</string>
<string name="screen_migration_message">"Il sagit dune opération ponctuelle, merci dattendre quelques instants."</string>
<string name="screen_migration_title">"Configuration de votre compte."</string>
<string name="screen_roomlist_a11y_create_message">"Créer une nouvelle discussion ou un nouveau salon"</string>
<string name="screen_roomlist_empty_message">"Commencez par envoyer un message à quelquun."</string>
<string name="screen_roomlist_empty_title">"Aucune discussion pour le moment."</string>

View file

@ -2,10 +2,18 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"A csevegés biztonsági mentése nincs szinkronban. Meg kell erősítened a helyreállítási kulcsát, hogy továbbra is hozzáférj a csevegés biztonsági mentéséhez."</string>
<string name="confirm_recovery_key_banner_title">"Helyreállítási kulcs megerősítése"</string>
<string name="screen_migration_message">"Ez egy egyszeri folyamat, köszönjük a türelmét."</string>
<string name="screen_migration_title">"A fiók beállítása."</string>
<string name="screen_roomlist_a11y_create_message">"Új beszélgetés vagy szoba létrehozása"</string>
<string name="screen_roomlist_empty_message">"Kezdje azzal, hogy üzenetet küld valakinek."</string>
<string name="screen_roomlist_empty_title">"Még nincsenek csevegések."</string>
<string name="screen_roomlist_filter_favourites">"Kedvencek"</string>
<string name="screen_roomlist_filter_low_priority">"Alacsony prioritás"</string>
<string name="screen_roomlist_filter_rooms">"Szobák"</string>
<string name="screen_roomlist_filter_unreads">"Olvasatlan"</string>
<string name="screen_roomlist_main_space_title">"Összes csevegés"</string>
<string name="screen_roomlist_mark_as_read">"Megjelölés olvasottként"</string>
<string name="screen_roomlist_mark_as_unread">"Megjelölés olvasatlanként"</string>
<string name="session_verification_banner_message">"Úgy tűnik, hogy új eszközt használ. Ellenőrizze egy másik eszközzel, hogy a továbbiakban elérje a titkosított üzeneteket."</string>
<string name="session_verification_banner_title">"Ellenőrizze, hogy Ön az"</string>
<string name="screen_roomlist_filter_people">"Emberek"</string>

View file

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Cadangan percakapan Anda saat ini tidak tersinkron. Anda perlu mengonfirmasi kunci pemulihan Anda untuk tetap memiliki akses ke cadangan percakapan Anda."</string>
<string name="confirm_recovery_key_banner_title">"Konfirmasi kunci pemulihan Anda"</string>
<string name="screen_migration_message">"Ini adalah proses satu kali, terima kasih telah menunggu."</string>
<string name="screen_migration_title">"Menyiapkan akun Anda."</string>
<string name="screen_roomlist_a11y_create_message">"Buat percakapan atau ruangan baru"</string>
<string name="screen_roomlist_empty_message">"Mulailah dengan mengirim pesan kepada seseorang."</string>
<string name="screen_roomlist_empty_title">"Belum ada obrolan."</string>

View file

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Il backup della chat non è attualmente sincronizzato. Devi confermare la chiave di recupero per mantenere l\'accesso al backup della chat."</string>
<string name="confirm_recovery_key_banner_title">"Conferma la chiave di recupero"</string>
<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_roomlist_a11y_create_message">"Crea una nuova conversazione o stanza"</string>
<string name="screen_roomlist_empty_message">"Inizia inviando un messaggio a qualcuno."</string>
<string name="screen_roomlist_empty_title">"Ancora nessuna chat."</string>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare."</string>
<string name="screen_migration_title">"Contul dumneavoastră se configurează"</string>
<string name="screen_roomlist_a11y_create_message">"Creați o conversație sau o cameră nouă"</string>
<string name="screen_roomlist_empty_message">"Începeți prin a trimite mesaje cuiva."</string>
<string name="screen_roomlist_empty_title">"Nu există încă discuții."</string>

View file

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"В настоящее время резервная копия вашего чата не синхронизирована. Требуется подтвердить вашим ключом восстановления, чтобы сохранить доступ к резервной копии чата."</string>
<string name="confirm_recovery_key_banner_title">"Подтвердите ключ восстановления"</string>
<string name="screen_migration_message">"Это одноразовый процесс, спасибо, что подождали."</string>
<string name="screen_migration_title">"Настройка учетной записи."</string>
<string name="screen_roomlist_a11y_create_message">"Создайте новую беседу или комнату"</string>
<string name="screen_roomlist_empty_message">"Начните переписку с отправки сообщения."</string>
<string name="screen_roomlist_empty_title">"Пока нет доступных чатов."</string>

View file

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Vaša záloha konverzácie nie je momentálne synchronizovaná. Na zachovanie prístupu k zálohe konverzácie musíte potvrdiť svoj kľúč na obnovu."</string>
<string name="confirm_recovery_key_banner_title">"Potvrďte svoj kľúč na obnovenie"</string>
<string name="screen_migration_message">"Ide o jednorazový proces, ďakujeme za trpezlivosť."</string>
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
<string name="screen_roomlist_a11y_create_message">"Vytvorte novú konverzáciu alebo miestnosť"</string>
<string name="screen_roomlist_empty_message">"Začnite tým, že niekomu pošlete správu."</string>
<string name="screen_roomlist_empty_title">"Zatiaľ žiadne konverzácie."</string>
@ -10,6 +12,8 @@
<string name="screen_roomlist_filter_rooms">"Miestnosti"</string>
<string name="screen_roomlist_filter_unreads">"Neprečítané"</string>
<string name="screen_roomlist_main_space_title">"Všetky konverzácie"</string>
<string name="screen_roomlist_mark_as_read">"Označiť ako prečítané"</string>
<string name="screen_roomlist_mark_as_unread">"Označiť ako neprečítané"</string>
<string name="session_verification_banner_message">"Vyzerá to tak, že používate nové zariadenie. Overte svoj prístup k zašifrovaným správam pomocou vášho druhého zariadenia."</string>
<string name="session_verification_banner_title">"Overte, že ste to vy"</string>
<string name="screen_roomlist_filter_people">"Ľudia"</string>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"這是一次性的程序,感謝您耐心等候。"</string>
<string name="screen_migration_title">"正在設定您的帳號。"</string>
<string name="screen_roomlist_a11y_create_message">"建立新的對話或聊天室"</string>
<string name="screen_roomlist_main_space_title">"所有聊天室"</string>
<string name="session_verification_banner_message">"您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。"</string>

View file

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup."</string>
<string name="confirm_recovery_key_banner_title">"Confirm your recovery key"</string>
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
<string name="screen_migration_title">"Setting up your account."</string>
<string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string>
<string name="screen_roomlist_empty_message">"Get started by messaging someone."</string>
<string name="screen_roomlist_empty_title">"No chats yet."</string>