First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.home.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
interface HomeEntryPoint : FeatureEntryPoint {
fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: Callback,
): Node
interface Callback : Plugin {
fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?)
fun navigateToCreateRoom()
fun navigateToSettings()
fun navigateToSetUpRecovery()
fun navigateToEnterRecoveryKey()
fun navigateToRoomSettings(roomId: RoomId)
fun navigateToBugReport()
}
}
+81
View File
@@ -0,0 +1,81 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.home.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
dependencies {
implementation(projects.appconfig)
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.eventformatter.api)
implementation(projects.libraries.indicator.api)
implementation(projects.libraries.deeplink.api)
implementation(projects.libraries.fullscreenintent.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.permissions.noop)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api)
implementation(projects.features.announcement.api)
implementation(projects.features.invite.api)
implementation(projects.features.networkmonitor.api)
implementation(projects.features.logout.api)
implementation(projects.features.leaveroom.api)
implementation(projects.features.rageshake.api)
implementation(projects.services.analytics.api)
implementation(libs.androidx.datastore.preferences)
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(projects.features.reportroom.api)
implementation(projects.features.rolesandpermissions.api)
implementation(projects.libraries.previewutils)
api(projects.features.home.api)
testCommonDependencies(libs, true)
testImplementation(projects.features.announcement.test)
testImplementation(projects.features.invite.test)
testImplementation(projects.features.logout.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.libraries.eventformatter.test)
testImplementation(projects.libraries.indicator.test)
testImplementation(projects.libraries.permissions.noop)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)
}
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.sessionstorage.api.SessionData
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
class CurrentUserWithNeighborsBuilder {
/**
* Build a list of [MatrixUser] containing the current user. If there are other sessions, the list
* will contain 3 users, with the current user in the middle.
* If there is only one other session, the list will contain twice the other user, to allow cycling.
*/
fun build(
matrixUser: MatrixUser,
sessions: List<SessionData>,
): ImmutableList<MatrixUser> {
// Sort by position to always have the same order (not depending on last account usage)
return sessions.sortedBy { it.position }
.map {
if (it.userId == matrixUser.userId.value) {
// Always use the freshest profile for the current user
matrixUser
} else {
// Use the data from the DB
MatrixUser(
userId = UserId(it.userId),
displayName = it.userDisplayName,
avatarUrl = it.userAvatarUrl,
)
}
}
.let { sessionList ->
// If the list has one item, there is no other session, return the list
when (sessionList.size) {
// Can happen when the user signs out (?)
0 -> listOf(matrixUser)
1 -> sessionList
else -> {
// Create a list with extra item at the start and end if necessary to have the current user in the middle
// If the list is [A, B, C, D] and the current user is A we want to return [D, A, B]
// If the current user is B, we want to return [A, B, C]
// If the current user is C, we want to return [B, C, D]
// If the current user is D, we want to return [C, D, A]
// Special case: if there are only two users, we want to return [B, A, B] or [A, B, A] to allows cycling
// between the two users.
val currentUserIndex = sessionList.indexOfFirst { it.userId == matrixUser.userId }
when (currentUserIndex) {
// This can happen when the user signs out.
// In this case, just return a singleton list with the current user.
-1 -> listOf(matrixUser)
0 -> listOf(sessionList.last()) + sessionList.take(2)
sessionList.lastIndex -> sessionList.takeLast(2) + sessionList.first()
else -> sessionList.slice(currentUserIndex - 1..currentUserIndex + 1)
}
}
}
}
.toImmutableList()
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultHomeEntryPoint : HomeEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: HomeEntryPoint.Callback,
): Node {
return parentNode.createNode<HomeFlowNode>(buildContext, listOf(callback))
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import io.element.android.libraries.matrix.api.core.SessionId
sealed interface HomeEvents {
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents
data class SwitchToAccount(val sessionId: SessionId) : HomeEvents
}
@@ -0,0 +1,283 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import android.app.Activity
import android.os.Parcelable
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.home.impl.components.RoomListMenuAction
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.features.leaveroom.api.LeaveRoomRenderer
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint
import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.utils.DelayedVisibility
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.milliseconds
@ContributesNode(SessionScope::class)
@AssistedInject
class HomeFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val matrixClient: MatrixClient,
private val presenter: HomePresenter,
private val inviteFriendsUseCase: InviteFriendsUseCase,
private val analyticsService: AnalyticsService,
private val acceptDeclineInviteView: AcceptDeclineInviteView,
private val directLogoutView: DirectLogoutView,
private val reportRoomEntryPoint: ReportRoomEntryPoint,
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
private val leaveRoomRenderer: LeaveRoomRenderer,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
) : BaseFlowNode<HomeFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
private val callback: HomeEntryPoint.Callback = callback()
private val stateFlow = launchMolecule { presenter.present() }
override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe(
onResume = {
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Home))
}
)
whenChildAttached {
commonLifecycle: Lifecycle,
changeRoomMemberRolesNode: ChangeRoomMemberRolesEntryPoint.NodeProxy,
->
commonLifecycle.coroutineScope.launch {
val isNewOwnerSelected = changeRoomMemberRolesNode.waitForCompletion()
withContext(NonCancellable) {
backstack.pop()
if (isNewOwnerSelected) {
onNewOwnersSelected(changeRoomMemberRolesNode.roomId)
}
}
}
}
}
sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget
@Parcelize
data class ReportRoom(val roomId: RoomId) : NavTarget
@Parcelize
data class DeclineInviteAndBlockUser(val inviteData: InviteData) : NavTarget
@Parcelize
data class SelectNewOwnersWhenLeavingRoom(val roomId: RoomId) : NavTarget
}
private fun navigateToReportRoom(roomId: RoomId) {
backstack.push(NavTarget.ReportRoom(roomId))
}
private fun navigateToDeclineInviteAndBlockUser(roomSummary: RoomListRoomSummary) {
backstack.push(NavTarget.DeclineInviteAndBlockUser(roomSummary.toInviteData()))
}
private fun onMenuActionClick(activity: Activity, roomListMenuAction: RoomListMenuAction) {
when (roomListMenuAction) {
RoomListMenuAction.InviteFriends -> {
inviteFriendsUseCase.execute(activity)
}
RoomListMenuAction.ReportBug -> {
callback.navigateToBugReport()
}
}
}
private fun navigateToSelectNewOwnersWhenLeavingRoom(roomId: RoomId) {
backstack.push(NavTarget.SelectNewOwnersWhenLeavingRoom(roomId))
}
private fun onNewOwnersSelected(roomId: RoomId) {
stateFlow.value.roomListState.eventSink(RoomListEvents.LeaveRoom(roomId, needsConfirmation = false))
}
private fun rootNode(buildContext: BuildContext): Node {
return node(buildContext) { modifier ->
val state by stateFlow.collectAsState()
val activity = requireNotNull(LocalActivity.current)
val loadingJoinedRoomJob = remember { mutableStateOf<AsyncData<Job>>(AsyncData.Uninitialized) }
if (loadingJoinedRoomJob.value.isLoading()) {
DelayedVisibility(duration = 400.milliseconds) {
ProgressDialog(
onDismissRequest = {
loadingJoinedRoomJob.value.dataOrNull()?.cancel()
loadingJoinedRoomJob.value = AsyncData.Uninitialized
}
)
}
}
fun navigateToRoom(
roomId: RoomId,
) {
if (!loadingJoinedRoomJob.value.isUninitialized()) {
Timber.w("Already loading a room, ignoring navigateToRoom for $roomId")
return
}
val job = sessionCoroutineScope.launch {
runCatchingExceptions {
matrixClient.getJoinedRoom(roomId)
}.fold(
onSuccess = { joinedRoom ->
if (isActive) {
callback.navigateToRoom(roomId, joinedRoom)
loadingJoinedRoomJob.value = AsyncData.Success(coroutineContext.job)
// Wait a bit before resetting the state to avoid allowing to open several rooms
delay(200.milliseconds)
loadingJoinedRoomJob.value = AsyncData.Uninitialized
}
},
onFailure = {
// If the operation wasn't cancelled, navigate without the room, using the room id
if (it !is CancellationException) {
callback.navigateToRoom(roomId, null)
}
loadingJoinedRoomJob.value = AsyncData.Failure(error = it, prevData = coroutineContext.job)
// Wait a bit before resetting the state to avoid allowing to open several rooms
delay(200.milliseconds)
loadingJoinedRoomJob.value = AsyncData.Uninitialized
}
)
}
loadingJoinedRoomJob.value = AsyncData.Loading(job)
}
HomeView(
homeState = state,
onRoomClick = ::navigateToRoom,
onSettingsClick = callback::navigateToSettings,
onStartChatClick = callback::navigateToCreateRoom,
onSetUpRecoveryClick = callback::navigateToSetUpRecovery,
onConfirmRecoveryKeyClick = callback::navigateToEnterRecoveryKey,
onRoomSettingsClick = callback::navigateToRoomSettings,
onMenuActionClick = { onMenuActionClick(activity, it) },
onReportRoomClick = ::navigateToReportRoom,
onDeclineInviteAndBlockUser = ::navigateToDeclineInviteAndBlockUser,
modifier = modifier,
acceptDeclineInviteView = {
acceptDeclineInviteView.Render(
state = state.roomListState.acceptDeclineInviteState,
onAcceptInviteSuccess = ::navigateToRoom,
onDeclineInviteSuccess = { },
modifier = Modifier
)
},
leaveRoomView = {
leaveRoomRenderer.Render(
state = state.roomListState.leaveRoomState,
onSelectNewOwners = ::navigateToSelectNewOwnersWhenLeavingRoom,
modifier = Modifier
)
}
)
directLogoutView.Render(state.directLogoutState)
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.ReportRoom -> {
reportRoomEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
roomId = navTarget.roomId,
)
}
is NavTarget.DeclineInviteAndBlockUser -> {
declineInviteAndBlockUserEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
inviteData = navTarget.inviteData,
)
}
is NavTarget.SelectNewOwnersWhenLeavingRoom -> {
val room = runBlocking { matrixClient.getJoinedRoom(navTarget.roomId) } ?: error("Room ${navTarget.roomId} not found")
changeRoomMemberRolesEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
room = room,
listType = ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving,
)
}
NavTarget.Root -> rootNode(buildContext)
}
}
}
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import io.element.android.compound.tokens.generated.CompoundIcons
enum class HomeNavigationBarItem(
@StringRes
val labelRes: Int,
) {
Chats(
labelRes = R.string.screen_home_tab_chats
),
Spaces(
labelRes = R.string.screen_home_tab_spaces
);
@Composable
fun icon(
isSelected: Boolean,
) = when (this) {
Chats -> if (isSelected) CompoundIcons.ChatSolid() else CompoundIcons.Chat()
Spaces -> if (isSelected) CompoundIcons.WorkspaceSolid() else CompoundIcons.Workspace()
}
companion object {
fun from(index: Int): HomeNavigationBarItem {
return entries.getOrElse(index) { Chats }
}
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@Inject
class HomePresenter(
private val client: MatrixClient,
private val syncService: SyncService,
private val snackbarDispatcher: SnackbarDispatcher,
private val indicatorService: IndicatorService,
private val roomListPresenter: Presenter<RoomListState>,
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService,
private val sessionStore: SessionStore,
private val announcementService: AnnouncementService,
) : Presenter<HomeState> {
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
@Composable
override fun present(): HomeState {
val coroutineState = rememberCoroutineScope()
val matrixUser by client.userProfile.collectAsState()
val currentUserAndNeighbors by remember {
combine(
client.userProfile,
sessionStore.sessionsFlow(),
currentUserWithNeighborsBuilder::build,
)
}.collectAsState(initial = persistentListOf(matrixUser))
val isOnline by syncService.isOnline.collectAsState()
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
val roomListState = roomListPresenter.present()
val homeSpacesState = homeSpacesPresenter.present()
val isSpaceFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
}.collectAsState(initial = false)
var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) }
val currentHomeNavigationBarItem by remember {
derivedStateOf {
HomeNavigationBarItem.from(currentHomeNavigationBarItemOrdinal)
}
}
LaunchedEffect(Unit) {
// Force a refresh of the profile
client.getUserProfile()
}
// Avatar indicator
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
val directLogoutState = logoutPresenter.present()
fun handleEvent(event: HomeEvents) {
when (event) {
is HomeEvents.SelectHomeNavigationBarItem -> coroutineState.launch {
if (event.item == HomeNavigationBarItem.Spaces) {
announcementService.showAnnouncement(Announcement.Space)
}
currentHomeNavigationBarItemOrdinal = event.item.ordinal
}
is HomeEvents.SwitchToAccount -> coroutineState.launch {
sessionStore.setLatestSession(event.sessionId.value)
}
}
}
LaunchedEffect(homeSpacesState.spaceRooms.isEmpty()) {
// If the last space is left, ensure that the Chat view is rendered.
if (homeSpacesState.spaceRooms.isEmpty()) {
currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal
}
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return HomeState(
currentUserAndNeighbors = currentUserAndNeighbors,
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = isOnline,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState,
homeSpacesState = homeSpacesState,
snackbarMessage = snackbarMessage,
canReportBug = canReportBug,
directLogoutState = directLogoutState,
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
eventSink = ::handleEvent,
)
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class HomeState(
/**
* The current user of this session, in case of multiple accounts, will contains 3 items, with the
* current user in the middle.
*/
val currentUserAndNeighbors: ImmutableList<MatrixUser>,
val showAvatarIndicator: Boolean,
val hasNetworkConnection: Boolean,
val currentHomeNavigationBarItem: HomeNavigationBarItem,
val roomListState: RoomListState,
val homeSpacesState: HomeSpacesState,
val snackbarMessage: SnackbarMessage?,
val canReportBug: Boolean,
val directLogoutState: DirectLogoutState,
val isSpaceFeatureEnabled: Boolean,
val eventSink: (HomeEvents) -> Unit,
) {
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
val showNavigationBar = isSpaceFeatureEnabled && homeSpacesState.spaceRooms.isNotEmpty()
}
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.roomlist.RoomListStateProvider
import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.roomlist.aRoomsContentState
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.home.impl.spaces.aHomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList
open class HomeStateProvider : PreviewParameterProvider<HomeState> {
override val values: Sequence<HomeState>
get() = sequenceOf(
aHomeState(),
aHomeState(hasNetworkConnection = false),
aHomeState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
aHomeState(
isSpaceFeatureEnabled = true,
roomListState = aRoomListState(
// Add more rooms to see the blur effect under the NavigationBar
contentState = aRoomsContentState(
summaries = generateRoomListRoomSummaryList(),
)
),
// For the bottom nav bar to be visible in the preview, the user must be member of at least one space
homeSpacesState = aHomeSpacesState(),
),
aHomeState(
isSpaceFeatureEnabled = true,
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
),
) + RoomListStateProvider().values.map {
aHomeState(roomListState = it)
}
}
internal fun aHomeState(
matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
currentUserAndNeighbors: List<MatrixUser> = listOf(matrixUser),
showAvatarIndicator: Boolean = false,
hasNetworkConnection: Boolean = true,
snackbarMessage: SnackbarMessage? = null,
currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats,
roomListState: RoomListState = aRoomListState(),
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
canReportBug: Boolean = true,
isSpaceFeatureEnabled: Boolean = false,
directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (HomeEvents) -> Unit = {}
) = HomeState(
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = snackbarMessage,
canReportBug = canReportBug,
directLogoutState = directLogoutState,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState,
homeSpacesState = homeSpacesState,
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
eventSink = eventSink,
)
@@ -0,0 +1,361 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalHazeMaterialsApi::class)
package io.element.android.features.home.impl
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.chrisbanes.haze.rememberHazeState
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.components.HomeTopBar
import io.element.android.features.home.impl.components.RoomListContentView
import io.element.android.features.home.impl.components.RoomListMenuAction
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.RoomListContextMenu
import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchView
import io.element.android.features.home.impl.spaces.HomeSpacesView
import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.NavigationBar
import io.element.android.libraries.designsystem.theme.components.NavigationBarIcon
import io.element.android.libraries.designsystem.theme.components.NavigationBarItem
import io.element.android.libraries.designsystem.theme.components.NavigationBarText
import io.element.android.libraries.designsystem.theme.components.Scaffold
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
import kotlinx.coroutines.launch
@Composable
fun HomeView(
homeState: HomeState,
onRoomClick: (RoomId) -> Unit,
onSettingsClick: () -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onStartChatClick: () -> Unit,
onRoomSettingsClick: (roomId: RoomId) -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onReportRoomClick: (roomId: RoomId) -> Unit,
onDeclineInviteAndBlockUser: (roomSummary: RoomListRoomSummary) -> Unit,
acceptDeclineInviteView: @Composable () -> Unit,
modifier: Modifier = Modifier,
leaveRoomView: @Composable () -> Unit,
) {
val state: RoomListState = homeState.roomListState
val coroutineScope = rememberCoroutineScope()
val firstThrottler = remember { FirstThrottler(300, coroutineScope) }
Box(modifier) {
if (state.contextMenu is RoomListState.ContextMenu.Shown) {
RoomListContextMenu(
contextMenu = state.contextMenu,
canReportRoom = state.canReportRoom,
eventSink = state.eventSink,
onRoomSettingsClick = onRoomSettingsClick,
onReportRoomClick = onReportRoomClick,
)
}
if (state.declineInviteMenu is RoomListState.DeclineInviteMenu.Shown) {
RoomListDeclineInviteMenu(
menu = state.declineInviteMenu,
canReportRoom = state.canReportRoom,
eventSink = state.eventSink,
onDeclineAndBlockClick = onDeclineInviteAndBlockUser,
)
}
leaveRoomView()
HomeScaffold(
state = homeState,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() },
onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
onMenuActionClick = onMenuActionClick,
)
// This overlaid view will only be visible when state.displaySearchResults is true
RoomListSearchView(
state = state.searchState,
eventSink = state.eventSink,
hideInvitesAvatars = state.hideInvitesAvatars,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
modifier = Modifier
.fillMaxSize()
.background(ElementTheme.colors.bgCanvasDefault)
)
acceptDeclineInviteView()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun HomeScaffold(
state: HomeState,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomId) -> Unit,
onOpenSettings: () -> Unit,
onStartChatClick: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
modifier: Modifier = Modifier,
) {
fun onRoomClick(room: RoomListRoomSummary) {
onRoomClick(room.roomId)
}
val appBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(appBarState)
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
val roomListState: RoomListState = state.roomListState
BackHandler(
enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats,
) {
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats))
}
val hazeState = rememberHazeState()
val roomsLazyListState = rememberLazyListState()
val spacesLazyListState = rememberLazyListState()
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
HomeTopBar(
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
currentUserAndNeighbors = state.currentUserAndNeighbors,
showAvatarIndicator = state.showAvatarIndicator,
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
onToggleSearch = { roomListState.eventSink(RoomListEvents.ToggleSearchResults) },
onMenuActionClick = onMenuActionClick,
onOpenSettings = onOpenSettings,
onAccountSwitch = {
state.eventSink(HomeEvents.SwitchToAccount(it))
},
scrollBehavior = scrollBehavior,
displayMenuItems = state.displayActions,
displayFilters = state.displayRoomListFilters,
filtersState = roomListState.filtersState,
canReportBug = state.canReportBug,
modifier = if (state.isSpaceFeatureEnabled) {
Modifier.hazeEffect(
state = hazeState,
style = HazeMaterials.thick(),
)
} else {
Modifier.background(ElementTheme.colors.bgCanvasDefault)
}
)
},
bottomBar = {
if (state.showNavigationBar) {
val coroutineScope = rememberCoroutineScope()
HomeBottomBar(
currentHomeNavigationBarItem = state.currentHomeNavigationBarItem,
onItemClick = { item ->
// scroll to top if selecting the same item
if (item == state.currentHomeNavigationBarItem) {
val lazyListStateTarget = when (item) {
HomeNavigationBarItem.Chats -> roomsLazyListState
HomeNavigationBarItem.Spaces -> spacesLazyListState
}
coroutineScope.launch {
if (lazyListStateTarget.firstVisibleItemIndex > 10) {
lazyListStateTarget.scrollToItem(10)
}
// Also reset the scrollBehavior height offset as it's not triggered by programmatic scrolls
scrollBehavior.state.heightOffset = 0f
lazyListStateTarget.animateScrollToItem(0)
}
} else {
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
}
},
modifier = Modifier.hazeEffect(
state = hazeState,
style = HazeMaterials.thick(),
)
)
}
},
content = { padding ->
when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> {
RoomListContentView(
contentState = roomListState.contentState,
filtersState = roomListState.filtersState,
lazyListState = roomsLazyListState,
hideInvitesAvatars = roomListState.hideInvitesAvatars,
eventSink = roomListState.eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
onCreateRoomClick = onStartChatClick,
contentPadding = PaddingValues(
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
// and include provided bottom padding
// Disable contentPadding due to navigation issue using the keyboard
// See https://issuetracker.google.com/issues/436432313
bottom = 80.dp,
// bottom = 80.dp + padding.calculateBottomPadding(),
// top = padding.calculateTopPadding()
),
modifier = Modifier
.padding(
PaddingValues(
start = padding.calculateStartPadding(LocalLayoutDirection.current),
end = padding.calculateEndPadding(LocalLayoutDirection.current),
// Remove these two lines once https://issuetracker.google.com/issues/436432313 has been fixed
bottom = padding.calculateBottomPadding(),
top = padding.calculateTopPadding()
)
)
.consumeWindowInsets(padding)
.hazeSource(state = hazeState)
)
}
HomeNavigationBarItem.Spaces -> {
HomeSpacesView(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
.hazeSource(state = hazeState),
state = state.homeSpacesState,
lazyListState = spacesLazyListState,
onSpaceClick = { spaceId ->
onRoomClick(spaceId)
}
)
}
}
},
floatingActionButton = {
if (state.displayActions) {
FloatingActionButton(
onClick = onStartChatClick,
) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message),
)
}
}
},
snackbarHost = { SnackbarHost(snackbarHostState) },
)
}
@Composable
private fun HomeBottomBar(
currentHomeNavigationBarItem: HomeNavigationBarItem,
onItemClick: (HomeNavigationBarItem) -> Unit,
modifier: Modifier = Modifier,
) {
NavigationBar(
containerColor = Color.Transparent,
modifier = modifier
) {
HomeNavigationBarItem.entries.forEach { item ->
val isSelected = currentHomeNavigationBarItem == item
NavigationBarItem(
selected = isSelected,
onClick = {
onItemClick(item)
},
icon = {
NavigationBarIcon(
imageVector = item.icon(isSelected),
)
},
label = {
NavigationBarText(
text = stringResource(item.labelRes),
)
}
)
}
}
}
internal fun RoomListRoomSummary.contentType() = displayType.ordinal
@PreviewsDayNight
@Composable
internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state: HomeState) = ElementPreview {
HomeView(
homeState = state,
onRoomClick = {},
onSettingsClick = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
onStartChatClick = {},
onRoomSettingsClick = {},
onReportRoomClick = {},
onMenuActionClick = {},
onDeclineInviteAndBlockUser = {},
acceptDeclineInviteView = {},
leaveRoomView = {}
)
}
@Preview
@Composable
internal fun HomeViewA11yPreview() = ElementPreview {
HomeView(
homeState = aHomeState(),
onRoomClick = {},
onSettingsClick = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
onStartChatClick = {},
onRoomSettingsClick = {},
onReportRoomClick = {},
onMenuActionClick = {},
onDeclineInviteAndBlockUser = {},
acceptDeclineInviteView = {},
leaveRoomView = {}
)
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Common padding for RoomList banners.
*/
internal fun Modifier.roomListBannerPadding() = padding(horizontal = 16.dp, vertical = 8.dp)
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.push.api.battery.BatteryOptimizationEvents
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
@Composable
internal fun BatteryOptimizationBanner(
state: BatteryOptimizationState,
modifier: Modifier = Modifier,
) {
Announcement(
modifier = modifier.roomListBannerPadding(),
title = stringResource(R.string.banner_battery_optimization_title_android),
description = stringResource(R.string.banner_battery_optimization_content_android),
type = AnnouncementType.Actionable(
actionText = stringResource(R.string.banner_battery_optimization_submit_android),
onActionClick = { state.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) },
onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) },
),
)
}
@PreviewsDayNight
@Composable
internal fun BatteryOptimizationBannerPreview() = ElementPreview {
BatteryOptimizationBanner(
state = aBatteryOptimizationState(),
)
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun ConfirmRecoveryKeyBanner(
onContinueClick: () -> Unit,
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Announcement(
modifier = modifier.roomListBannerPadding(),
title = stringResource(R.string.confirm_recovery_key_banner_title),
description = stringResource(R.string.confirm_recovery_key_banner_message),
type = AnnouncementType.Actionable(
actionText = stringResource(CommonStrings.action_continue),
onActionClick = onContinueClick,
onDismissClick = onDismissClick,
),
)
}
@PreviewsDayNight
@Composable
internal fun ConfirmRecoveryKeyBannerPreview() = ElementPreview {
ConfirmRecoveryKeyBanner(
onContinueClick = {},
onDismissClick = {},
)
}
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun FullScreenIntentPermissionBanner(
state: FullScreenIntentPermissionsState,
modifier: Modifier = Modifier
) {
Announcement(
title = stringResource(R.string.full_screen_intent_banner_title),
description = stringResource(R.string.full_screen_intent_banner_message),
type = AnnouncementType.Actionable(
actionText = stringResource(CommonStrings.action_continue),
onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvents.Dismiss) },
onActionClick = { state.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) },
),
modifier = modifier.roomListBannerPadding(),
)
}
@PreviewsDayNight
@Composable
internal fun FullScreenIntentPermissionBannerPreview() {
ElementPreview {
FullScreenIntentPermissionBanner(aFullScreenIntentPermissionsState())
}
}
@@ -0,0 +1,332 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import io.element.android.appconfig.RoomListConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.RoomListFiltersView
import io.element.android.features.home.impl.filters.aRoomListFiltersState
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.modifiers.backgroundVerticalGradient
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
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.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeTopBar(
title: String,
currentUserAndNeighbors: ImmutableList<MatrixUser>,
showAvatarIndicator: Boolean,
areSearchResultsDisplayed: Boolean,
onToggleSearch: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onOpenSettings: () -> Unit,
onAccountSwitch: (SessionId) -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
displayMenuItems: Boolean,
canReportBug: Boolean,
displayFilters: Boolean,
filtersState: RoomListFiltersState,
modifier: Modifier = Modifier,
) {
Column(modifier) {
TopAppBar(
modifier = Modifier
.backgroundVerticalGradient(
isVisible = !areSearchResultsDisplayed,
)
.statusBarsPadding(),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
),
title = {
Text(
modifier = Modifier.semantics {
heading()
},
style = ElementTheme.typography.aliasScreenTitle,
text = title,
)
},
navigationIcon = {
NavigationIcon(
currentUserAndNeighbors = currentUserAndNeighbors,
showAvatarIndicator = showAvatarIndicator,
onAccountSwitch = onAccountSwitch,
onClick = onOpenSettings,
)
},
actions = {
if (displayMenuItems) {
IconButton(
onClick = onToggleSearch,
) {
Icon(
imageVector = CompoundIcons.Search(),
contentDescription = stringResource(CommonStrings.action_search),
)
}
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
var showMenu by remember { mutableStateOf(false) }
IconButton(
onClick = { showMenu = !showMenu }
) {
Icon(
imageVector = CompoundIcons.OverflowVertical(),
contentDescription = null,
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClick(RoomListMenuAction.InviteFriends)
},
text = { Text(stringResource(id = CommonStrings.action_invite)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ShareAndroid(),
tint = ElementTheme.colors.iconSecondary,
contentDescription = null,
)
}
)
}
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM && canReportBug) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClick(RoomListMenuAction.ReportBug)
},
text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ChatProblem(),
tint = ElementTheme.colors.iconSecondary,
contentDescription = null,
)
}
)
}
}
}
}
},
// We want a 16dp left padding for the navigationIcon :
// 4dp from default TopAppBarHorizontalPadding
// 8dp from AccountIcon default padding (because of IconButton)
// 4dp extra padding using left insets
windowInsets = WindowInsets(left = 4.dp),
)
if (displayFilters) {
TopAppBarScrollBehaviorLayout(scrollBehavior = scrollBehavior) {
RoomListFiltersView(
state = filtersState,
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
}
}
@Composable
private fun NavigationIcon(
currentUserAndNeighbors: ImmutableList<MatrixUser>,
showAvatarIndicator: Boolean,
onAccountSwitch: (SessionId) -> Unit,
onClick: () -> Unit,
) {
if (currentUserAndNeighbors.size == 1) {
AccountIcon(
matrixUser = currentUserAndNeighbors.single(),
isCurrentAccount = true,
showAvatarIndicator = showAvatarIndicator,
onClick = onClick,
)
} else {
// Render a vertical pager
val pagerState = rememberPagerState(initialPage = 1) { currentUserAndNeighbors.size }
// Listen to page changes and switch account if needed
val latestOnAccountSwitch by rememberUpdatedState(onAccountSwitch)
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.settledPage }.collect { page ->
latestOnAccountSwitch(SessionId(currentUserAndNeighbors[page].userId.value))
}
}
VerticalPager(
state = pagerState,
modifier = Modifier.height(48.dp),
) { page ->
AccountIcon(
matrixUser = currentUserAndNeighbors[page],
isCurrentAccount = page == 1,
showAvatarIndicator = page == 1 && showAvatarIndicator,
onClick = if (page == 1) {
onClick
} else {
{}
},
)
}
}
}
@Composable
private fun AccountIcon(
matrixUser: MatrixUser,
isCurrentAccount: Boolean,
showAvatarIndicator: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val testTag = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier
IconButton(
modifier = modifier.then(testTag),
onClick = onClick,
) {
Box {
val avatarData by remember(matrixUser) {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
}
}
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null,
)
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier.align(Alignment.TopEnd)
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarPreview() = ElementPreview {
HomeTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
displayMenuItems = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
onMenuActionClick = {},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
HomeTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")),
showAvatarIndicator = true,
areSearchResultsDisplayed = false,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
displayMenuItems = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
onMenuActionClick = {},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@PreviewsDayNight
@Composable
internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
HomeTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
displayMenuItems = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
onMenuActionClick = {},
)
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun NewNotificationSoundBanner(
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Announcement(
modifier = modifier.roomListBannerPadding(),
title = stringResource(R.string.banner_new_sound_title),
description = stringResource(R.string.banner_new_sound_message),
type = AnnouncementType.Actionable(
actionText = stringResource(CommonStrings.action_ok),
onActionClick = onDismissClick,
onDismissClick = onDismissClick,
),
)
}
@PreviewsDayNight
@Composable
internal fun NewNotificationSoundBannerPreview() = ElementPreview {
NewNotificationSoundBanner(
onDismissClick = {},
)
}
@@ -0,0 +1,355 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
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.home.impl.R
import io.element.android.features.home.impl.contentType
import io.element.android.features.home.impl.filters.RoomListFilter
import io.element.android.features.home.impl.filters.RoomListFiltersEmptyStateResources
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.aRoomListFiltersState
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.roomlist.RoomListContentState
import io.element.android.features.home.impl.roomlist.RoomListContentStateProvider
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.features.home.impl.roomlist.SecurityBannerState
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
@Composable
fun RoomListContentView(
contentState: RoomListContentState,
filtersState: RoomListFiltersState,
lazyListState: LazyListState,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
onCreateRoomClick: () -> Unit,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
when (contentState) {
is RoomListContentState.Skeleton -> {
SkeletonView(
modifier = modifier,
count = contentState.count,
contentPadding = contentPadding,
)
}
is RoomListContentState.Empty -> {
EmptyView(
modifier = modifier.padding(contentPadding),
state = contentState,
eventSink = eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onCreateRoomClick = onCreateRoomClick,
)
}
is RoomListContentState.Rooms -> {
RoomsView(
modifier = modifier,
state = contentState,
hideInvitesAvatars = hideInvitesAvatars,
filtersState = filtersState,
eventSink = eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick,
lazyListState = lazyListState,
contentPadding = contentPadding,
)
}
}
}
@Composable
private fun SkeletonView(
count: Int,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
contentPadding = contentPadding,
) {
repeat(count) { index ->
item {
RoomSummaryPlaceholderRow()
if (index != count - 1) {
HorizontalDivider()
}
}
}
}
}
@Composable
private fun EmptyView(
state: RoomListContentState.Empty,
eventSink: (RoomListEvents) -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onCreateRoomClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier.fillMaxSize()) {
EmptyScaffold(
title = R.string.screen_roomlist_empty_title,
subtitle = R.string.screen_roomlist_empty_message,
action = {
Button(
text = stringResource(CommonStrings.action_start_chat),
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
onClick = onCreateRoomClick,
)
},
modifier = Modifier.align(Alignment.Center),
)
Box {
when (state.securityBannerState) {
SecurityBannerState.SetUpRecovery -> {
SetUpRecoveryKeyBanner(
onContinueClick = onSetUpRecoveryClick,
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
)
}
SecurityBannerState.RecoveryKeyConfirmation -> {
ConfirmRecoveryKeyBanner(
onContinueClick = onConfirmRecoveryKeyClick,
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
)
}
SecurityBannerState.None -> Unit
}
}
}
}
@Composable
private fun RoomsView(
state: RoomListContentState.Rooms,
hideInvitesAvatars: Boolean,
filtersState: RoomListFiltersState,
eventSink: (RoomListEvents) -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
contentPadding: PaddingValues,
lazyListState: LazyListState,
modifier: Modifier = Modifier,
) {
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
EmptyViewForFilterStates(
selectedFilters = filtersState.selectedFilters(),
modifier = modifier.fillMaxSize()
)
} else {
RoomsViewList(
state = state,
hideInvitesAvatars = hideInvitesAvatars,
eventSink = eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick,
contentPadding = contentPadding,
lazyListState = lazyListState,
modifier = modifier.fillMaxSize(),
)
}
}
@Composable
private fun RoomsViewList(
state: RoomListContentState.Rooms,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
contentPadding: PaddingValues,
lazyListState: LazyListState,
modifier: Modifier = Modifier,
) {
val visibleRange by remember {
derivedStateOf {
val layoutInfo = lazyListState.layoutInfo
val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0
val size = layoutInfo.visibleItemsInfo.size
firstItemIndex until firstItemIndex + size
}
}
val updatedEventSink by rememberUpdatedState(newValue = eventSink)
LaunchedEffect(visibleRange) {
updatedEventSink(RoomListEvents.UpdateVisibleRange(visibleRange))
}
LazyColumn(
state = lazyListState,
modifier = modifier,
contentPadding = contentPadding,
) {
when (state.securityBannerState) {
SecurityBannerState.SetUpRecovery -> {
item {
SetUpRecoveryKeyBanner(
onContinueClick = onSetUpRecoveryClick,
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) },
)
}
}
SecurityBannerState.RecoveryKeyConfirmation -> {
item {
ConfirmRecoveryKeyBanner(
onContinueClick = onConfirmRecoveryKeyClick,
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) },
)
}
}
SecurityBannerState.None -> if (state.fullScreenIntentPermissionsState.shouldDisplayBanner) {
item {
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
}
} else if (state.batteryOptimizationState.shouldDisplayBanner) {
item {
BatteryOptimizationBanner(state = state.batteryOptimizationState)
}
} else if (state.showNewNotificationSoundBanner) {
item {
NewNotificationSoundBanner(
onDismissClick = { updatedEventSink(RoomListEvents.DismissNewNotificationSoundBanner) },
)
}
}
}
// Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room
// is moved to the top of the list.
itemsIndexed(
items = state.summaries,
contentType = { _, room -> room.contentType() },
) { index, room ->
RoomSummaryRow(
room = room,
hideInviteAvatars = hideInvitesAvatars,
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
state.seenRoomInvites.contains(room.roomId),
onClick = onRoomClick,
eventSink = eventSink,
)
if (index != state.summaries.lastIndex) {
HorizontalDivider()
}
}
}
}
@Composable
private fun EmptyViewForFilterStates(
selectedFilters: ImmutableList<RoomListFilter>,
modifier: Modifier = Modifier,
) {
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return
EmptyScaffold(
title = emptyStateResources.title,
subtitle = emptyStateResources.subtitle,
modifier = modifier,
)
}
@Composable
private fun EmptyScaffold(
@StringRes title: Int,
@StringRes subtitle: Int,
modifier: Modifier = Modifier,
action: @Composable (ColumnScope.() -> Unit)? = null,
) {
Column(
modifier = modifier.padding(horizontal = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(title),
style = ElementTheme.typography.fontHeadingMdBold,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(subtitle),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(32.dp))
action?.invoke(this)
}
}
@PreviewsDayNight
@Composable
internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStateProvider::class) state: RoomListContentState) = ElementPreview {
RoomListContentView(
contentState = state,
filtersState = aRoomListFiltersState(
filterSelectionStates = RoomListFilter.entries.map {
FilterSelectionState(
filter = it,
isSelected = true
)
}
),
hideInvitesAvatars = false,
eventSink = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
onRoomClick = {},
onCreateRoomClick = {},
lazyListState = rememberLazyListState(),
contentPadding = PaddingValues(0.dp),
)
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
enum class RoomListMenuAction {
InviteFriends,
ReportBug
}
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.placeholderBackground
/**
* https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=6547%3A147623
*/
@Composable
internal fun RoomSummaryPlaceholderRow(
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.height(minHeight)
.padding(horizontal = 16.dp),
) {
Box(
modifier = Modifier
.size(AvatarSize.RoomListItem.dp)
.align(Alignment.CenterVertically)
.background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, top = 19.dp, end = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(22.dp),
verticalAlignment = Alignment.CenterVertically
) {
PlaceholderAtom(width = 40.dp, height = 7.dp)
Spacer(modifier = Modifier.width(7.dp))
PlaceholderAtom(width = 45.dp, height = 7.dp)
Spacer(modifier = Modifier.weight(1f))
PlaceholderAtom(width = 22.dp, height = 4.dp)
}
Row(
modifier = Modifier
.height(25.dp),
verticalAlignment = Alignment.CenterVertically
) {
PlaceholderAtom(width = 70.dp, height = 6.dp)
Spacer(modifier = Modifier.width(6.dp))
PlaceholderAtom(width = 70.dp, height = 6.dp)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun RoomSummaryPlaceholderRowPreview() = ElementPreview {
RoomSummaryPlaceholderRow()
}
@@ -0,0 +1,445 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.model.LatestEvent
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomListRoomSummaryProvider
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.matrix.ui.model.InviteSender
import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber
internal val minHeight = 84.dp
@Composable
internal fun RoomSummaryRow(
room: RoomListRoomSummary,
hideInviteAvatars: Boolean,
isInviteSeen: Boolean,
onClick: (RoomListRoomSummary) -> Unit,
eventSink: (RoomListEvents) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
when (room.displayType) {
RoomSummaryDisplayType.PLACEHOLDER -> {
RoomSummaryPlaceholderRow()
}
RoomSummaryDisplayType.INVITE -> {
RoomSummaryScaffoldRow(
room = room,
hideAvatarImage = hideInviteAvatars,
onClick = onClick,
onLongClick = {
Timber.d("Long click on invite room")
},
) {
InviteNameAndIndicatorRow(name = room.name, isInviteSeen = isInviteSeen)
InviteSubtitle(isDm = room.isDm, inviteSender = room.inviteSender)
if (!room.isDm && room.inviteSender != null) {
Spacer(modifier = Modifier.height(4.dp))
InviteSenderView(
modifier = Modifier.fillMaxWidth(),
inviteSender = room.inviteSender,
hideAvatarImage = hideInviteAvatars
)
}
Spacer(modifier = Modifier.height(12.dp))
InviteButtonsRowMolecule(
onAcceptClick = {
eventSink(RoomListEvents.AcceptInvite(room))
},
onDeclineClick = {
eventSink(RoomListEvents.ShowDeclineInviteMenu(room))
}
)
}
}
RoomSummaryDisplayType.ROOM -> {
RoomSummaryScaffoldRow(
room = room,
onClick = onClick,
onLongClick = {
eventSink(RoomListEvents.ShowContextMenu(room))
},
) {
NameAndTimestampRow(
name = room.name,
latestEvent = room.latestEvent,
timestamp = room.timestamp,
isHighlighted = room.isHighlighted
)
MessagePreviewAndIndicatorRow(room = room)
}
}
RoomSummaryDisplayType.KNOCKED -> {
RoomSummaryScaffoldRow(
room = room,
onClick = onClick,
onLongClick = {
Timber.d("Long click on knocked room")
},
) {
NameAndTimestampRow(
name = room.name,
latestEvent = room.latestEvent,
timestamp = null,
isHighlighted = room.isHighlighted
)
if (room.canonicalAlias != null) {
Text(
text = room.canonicalAlias.value,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
)
Spacer(modifier = Modifier.height(4.dp))
}
Text(
text = stringResource(id = R.string.screen_roomlist_knock_event_sent_description),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
)
}
}
}
}
}
@Composable
private fun RoomSummaryScaffoldRow(
room: RoomListRoomSummary,
onClick: (RoomListRoomSummary) -> Unit,
onLongClick: (RoomListRoomSummary) -> Unit,
modifier: Modifier = Modifier,
hideAvatarImage: Boolean = false,
content: @Composable ColumnScope.() -> Unit
) {
val clickModifier = Modifier
.combinedClickable(
onClick = { onClick(room) },
onLongClick = { onLongClick(room) },
onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
indication = ripple(),
interactionSource = remember { MutableInteractionSource() }
)
.onKeyboardContextMenuAction { onLongClick(room) }
Row(
modifier = modifier
.fillMaxWidth()
.heightIn(min = minHeight)
.then(clickModifier)
.padding(horizontal = 16.dp, vertical = 11.dp)
.height(IntrinsicSize.Min),
) {
Avatar(
avatarData = room.avatarData,
avatarType = if (room.isSpace) {
AvatarType.Space(isTombstoned = room.isTombstoned)
} else {
AvatarType.Room(
heroes = room.heroes,
isTombstoned = room.isTombstoned,
)
},
hideImage = hideAvatarImage,
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.fillMaxWidth(),
content = content,
)
}
}
@Composable
private fun NameAndTimestampRow(
name: String?,
latestEvent: LatestEvent,
timestamp: String?,
isHighlighted: Boolean,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = spacedBy(16.dp)
) {
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
) {
// Name
Text(
style = ElementTheme.typography.fontBodyLgMedium,
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
color = ElementTheme.colors.roomListRoomName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Picto
when (latestEvent) {
is LatestEvent.Sending -> {
Spacer(modifier = Modifier.width(4.dp))
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.Time(),
contentDescription = null,
tint = ElementTheme.colors.iconTertiary,
)
}
is LatestEvent.Error -> {
Spacer(modifier = Modifier.width(4.dp))
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.ErrorSolid(),
contentDescription = null,
tint = ElementTheme.colors.iconCriticalPrimary,
)
}
else -> Unit
}
}
// Timestamp
Text(
text = timestamp ?: "",
style = ElementTheme.typography.fontBodySmMedium,
color = if (isHighlighted) {
ElementTheme.colors.unreadIndicator
} else {
ElementTheme.colors.roomListRoomMessageDate
},
)
}
}
@Composable
private fun InviteSubtitle(
isDm: Boolean,
inviteSender: InviteSender?,
modifier: Modifier = Modifier
) {
val subtitle = if (isDm) {
inviteSender?.userId?.value
} else {
null
}
if (subtitle != null) {
Text(
text = subtitle,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.roomListRoomMessage,
modifier = modifier,
)
}
}
@Composable
private fun MessagePreviewAndIndicatorRow(
room: RoomListRoomSummary,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = spacedBy(28.dp)
) {
if (room.isTombstoned) {
Text(
modifier = Modifier.weight(1f),
text = stringResource(R.string.screen_roomlist_tombstoned_room_description),
color = ElementTheme.colors.roomListRoomMessage,
style = ElementTheme.typography.fontBodyMdRegular,
minLines = 2,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
} else {
if (room.latestEvent is LatestEvent.Error) {
Text(
modifier = Modifier.weight(1f),
text = stringResource(CommonStrings.common_message_failed_to_send),
color = ElementTheme.colors.textCriticalPrimary,
style = ElementTheme.typography.fontBodyMdRegular,
minLines = 2,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
} else {
val messagePreview = room.latestEvent.content()
val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.orEmpty().toString())
Text(
modifier = Modifier.weight(1f),
text = annotatedMessagePreview,
color = ElementTheme.colors.roomListRoomMessage,
style = ElementTheme.typography.fontBodyMdRegular,
minLines = 2,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
// Call and unread
Row(
modifier = Modifier
.height(16.dp)
// Used to force this line to be read aloud earlier than the latest event when using Talkback
.zIndex(-1f),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val tint = if (room.isHighlighted) ElementTheme.colors.unreadIndicator else ElementTheme.colors.iconQuaternary
if (room.hasRoomCall) {
OnGoingCallIcon(
color = tint,
)
}
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
NotificationOffIndicatorAtom()
} else if (room.numberOfUnreadMentions > 0) {
MentionIndicatorAtom()
}
if (room.hasNewContent) {
val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages)
UnreadIndicatorAtom(
color = tint,
contentDescription = contentDescription,
)
}
}
}
}
@Composable
private fun InviteNameAndIndicatorRow(
name: String?,
isInviteSeen: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium,
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
color = ElementTheme.colors.roomListRoomName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (!isInviteSeen) {
UnreadIndicatorAtom(
color = ElementTheme.colors.unreadIndicator
)
}
}
}
@Composable
private fun OnGoingCallIcon(
color: Color,
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_notifications_ongoing_call),
tint = color,
)
}
@Composable
private fun NotificationOffIndicatorAtom() {
Icon(
modifier = Modifier.size(16.dp),
contentDescription = stringResource(CommonStrings.a11y_notifications_muted),
imageVector = CompoundIcons.NotificationsOffSolid(),
tint = ElementTheme.colors.iconQuaternary,
)
}
@Composable
private fun MentionIndicatorAtom() {
Icon(
modifier = Modifier.size(16.dp),
contentDescription = stringResource(CommonStrings.a11y_notifications_new_mentions),
imageVector = CompoundIcons.Mention(),
tint = ElementTheme.colors.unreadIndicator,
)
}
@PreviewsDayNight
@Composable
internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryProvider::class) data: RoomListRoomSummary) = ElementPreview {
RoomSummaryRow(
room = data,
hideInviteAvatars = false,
// Set isInviteSeen to true for the preview when the room has name "Bob"
isInviteSeen = data.name == "Bob",
onClick = {},
eventSink = {},
)
}
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@Composable
internal fun SetUpRecoveryKeyBanner(
onContinueClick: () -> Unit,
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Announcement(
modifier = modifier.roomListBannerPadding(),
title = stringResource(R.string.banner_set_up_recovery_title),
description = stringResource(R.string.banner_set_up_recovery_content),
type = AnnouncementType.Actionable(
actionText = stringResource(R.string.banner_set_up_recovery_submit),
onActionClick = onContinueClick,
onDismissClick = onDismissClick,
),
)
}
@PreviewsDayNight
@Composable
internal fun SetUpRecoveryKeyBannerPreview() = ElementPreview {
SetUpRecoveryKeyBanner(
onContinueClick = {},
onDismissClick = {},
)
}
@@ -0,0 +1,161 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.datasource
import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
import io.element.android.libraries.androidutils.system.DateTimeObserver
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@Inject
class RoomListDataSource(
private val roomListService: RoomListService,
private val roomListRoomSummaryFactory: RoomListRoomSummaryFactory,
private val coroutineDispatchers: CoroutineDispatchers,
private val notificationSettingsService: NotificationSettingsService,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val dateTimeObserver: DateTimeObserver,
) {
init {
observeNotificationSettings()
observeDateTimeChanges()
}
private val _allRooms = MutableSharedFlow<ImmutableList<RoomListRoomSummary>>(replay = 1)
private val lock = Mutex()
private val diffCache = MutableListDiffCache<RoomListRoomSummary>()
private val diffCacheUpdater = DiffCacheUpdater<RoomSummary, RoomListRoomSummary>(diffCache = diffCache, detectMoves = true) { old, new ->
old?.roomId == new?.roomId
}
val allRooms: Flow<ImmutableList<RoomListRoomSummary>> = _allRooms
val loadingState = roomListService.allRooms.loadingState
fun launchIn(coroutineScope: CoroutineScope) {
roomListService
.allRooms
.filteredSummaries
.onEach { roomSummaries ->
replaceWith(roomSummaries)
}
.launchIn(coroutineScope)
}
suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
roomListService.subscribeToVisibleRooms(roomIds)
}
@OptIn(FlowPreview::class)
private fun observeNotificationSettings() {
notificationSettingsService.notificationSettingsChangeFlow
.debounce(0.5.seconds)
.onEach {
roomListService.allRooms.rebuildSummaries()
}
.launchIn(sessionCoroutineScope)
}
private fun observeDateTimeChanges() {
dateTimeObserver.changes
.onEach { event ->
when (event) {
is DateTimeObserver.Event.TimeZoneChanged -> rebuildAllRoomSummaries()
is DateTimeObserver.Event.DateChanged -> rebuildAllRoomSummaries()
}
}
.launchIn(sessionCoroutineScope)
}
private suspend fun replaceWith(roomSummaries: List<RoomSummary>) = withContext(coroutineDispatchers.computation) {
lock.withLock {
diffCacheUpdater.updateWith(roomSummaries)
buildAndEmitAllRooms(roomSummaries)
}
}
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
// Used to detect duplicates in the room list summaries - see comment below
data class CacheResult(val index: Int, val fromCache: Boolean)
val cachingResults = mutableMapOf<RoomId, MutableList<CacheResult>>()
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
if (useCache) {
diffCache.get(index)?.let { cachedItem ->
// Add the cached item to the caching results
val pairs = cachingResults.getOrDefault(cachedItem.roomId, mutableListOf())
pairs.add(CacheResult(index, fromCache = true))
cachingResults[cachedItem.roomId] = pairs
cachedItem
} ?: run {
roomSummaries.getOrNull(index)?.roomId?.let {
// Add the non-cached item to the caching results
val pairs = cachingResults.getOrDefault(it, mutableListOf())
pairs.add(CacheResult(index, fromCache = false))
cachingResults[it] = pairs
}
buildAndCacheItem(roomSummaries, index)
}
} else {
roomSummaries.getOrNull(index)?.roomId?.let {
// Add the non-cached item to the caching results
val pairs = cachingResults.getOrDefault(it, mutableListOf())
pairs.add(CacheResult(index, fromCache = false))
cachingResults[it] = pairs
}
buildAndCacheItem(roomSummaries, index)
}
}
// TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed
val duplicates = cachingResults.filter { (_, operations) -> operations.size > 1 }
if (duplicates.isNotEmpty()) {
Timber.e("Found duplicates in room summaries after an UI update: $duplicates. This could be a race condition/caching issue of some kind")
}
_allRooms.emit(roomListRoomSummaries.toImmutableList())
}
private fun buildAndCacheItem(roomSummaries: List<RoomSummary>, index: Int): RoomListRoomSummary? {
val roomListSummary = roomSummaries.getOrNull(index)?.let { roomListRoomSummaryFactory.create(it) }
diffCache[index] = roomListSummary
return roomListSummary
}
private suspend fun rebuildAllRoomSummaries() {
lock.withLock {
roomListService.allRooms.filteredSummaries.replayCache.firstOrNull()?.let { roomSummaries ->
buildAndEmitAllRooms(roomSummaries, useCache = false)
}
}
}
}
@@ -0,0 +1,100 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.datasource
import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.model.LatestEvent
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.toInviteSender
import kotlinx.collections.immutable.toImmutableList
@Inject
class RoomListRoomSummaryFactory(
private val dateFormatter: DateFormatter,
private val roomLatestEventFormatter: RoomLatestEventFormatter,
) {
fun create(roomSummary: RoomSummary): RoomListRoomSummary {
val roomInfo = roomSummary.info
val avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomListItem)
return RoomListRoomSummary(
id = roomSummary.roomId.value,
roomId = roomSummary.roomId,
name = roomInfo.name,
numberOfUnreadMessages = roomInfo.numUnreadMessages,
numberOfUnreadMentions = roomInfo.numUnreadMentions,
numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
isMarkedUnread = roomInfo.isMarkedUnread,
timestamp = dateFormatter.format(
timestamp = roomSummary.latestEventTimestamp,
mode = DateFormatterMode.TimeOrDate,
useRelative = true,
),
latestEvent = computeLatestEvent(roomSummary.latestEvent, roomInfo.isDm),
avatarData = avatarData,
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
hasRoomCall = roomInfo.hasRoomCall,
isDirect = roomInfo.isDirect,
isFavorite = roomInfo.isFavorite,
inviteSender = roomInfo.inviter?.toInviteSender(),
isDm = roomInfo.isDm,
canonicalAlias = roomInfo.canonicalAlias,
displayType = when (roomInfo.currentUserMembership) {
CurrentUserMembership.INVITED -> {
RoomSummaryDisplayType.INVITE
}
CurrentUserMembership.KNOCKED -> {
RoomSummaryDisplayType.KNOCKED
}
else -> {
RoomSummaryDisplayType.ROOM
}
},
heroes = roomInfo.heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomListItem)
}.toImmutableList(),
isTombstoned = roomInfo.successorRoom != null,
isSpace = roomInfo.isSpace,
)
}
private fun computeLatestEvent(latestEvent: LatestEventValue, dm: Boolean): LatestEvent {
return when (latestEvent) {
is LatestEventValue.None -> {
LatestEvent.None
}
is LatestEventValue.Local -> {
if (latestEvent.isSending) {
val content = roomLatestEventFormatter.format(latestEvent, dm).orEmpty()
LatestEvent.Sending(
content = content,
)
} else {
LatestEvent.Error
}
}
is LatestEventValue.Remote -> {
val content = roomLatestEventFormatter.format(latestEvent, dm).orEmpty()
LatestEvent.Synced(
content = content,
)
}
}
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.home.impl.spaces.HomeSpacesPresenter
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@BindingContainer
@ContributesTo(SessionScope::class)
interface HomeSpacesModule {
@Binds
fun bindHomeSpacesPresenter(presenter: HomeSpacesPresenter): Presenter<HomeSpacesState>
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.home.impl.filters.RoomListFiltersPresenter
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.roomlist.RoomListPresenter
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchPresenter
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
@BindingContainer
interface RoomListModule {
@Binds
fun bindRoomListPresenter(presenter: RoomListPresenter): Presenter<RoomListState>
@Binds
fun bindSearchPresenter(presenter: RoomListSearchPresenter): Presenter<RoomListSearchState>
@Binds
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters
import io.element.android.features.home.impl.R
/**
* Enum class representing the different filters that can be applied to the room list.
* Order is important, it'll be used as initial order in the UI.
*/
enum class RoomListFilter(val stringResource: Int) {
Unread(R.string.screen_roomlist_filter_unreads),
People(R.string.screen_roomlist_filter_people),
Rooms(R.string.screen_roomlist_filter_rooms),
Favourites(R.string.screen_roomlist_filter_favourites),
Invites(R.string.screen_roomlist_filter_invites);
val incompatibleFilters: Set<RoomListFilter>
get() = when (this) {
Rooms -> setOf(People, Invites)
People -> setOf(Rooms, Invites)
Unread -> setOf(Invites)
Favourites -> setOf(Invites)
Invites -> setOf(Rooms, People, Unread, Favourites)
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters
import androidx.annotation.StringRes
import io.element.android.features.home.impl.R
/**
* Holds the resources for the empty state when filters are applied to the room list.
* @param title the title of the empty state
* @param subtitle the subtitle of the empty state
*/
data class RoomListFiltersEmptyStateResources(
@StringRes val title: Int,
@StringRes val subtitle: Int,
) {
companion object {
/**
* Create a [RoomListFiltersEmptyStateResources] from a list of selected filters.
*/
fun fromSelectedFilters(selectedFilters: List<RoomListFilter>): RoomListFiltersEmptyStateResources? {
return when {
selectedFilters.isEmpty() -> null
selectedFilters.size == 1 -> {
when (selectedFilters.first()) {
RoomListFilter.Unread -> RoomListFiltersEmptyStateResources(
title = R.string.screen_roomlist_filter_unreads_empty_state_title,
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
)
RoomListFilter.People -> RoomListFiltersEmptyStateResources(
title = R.string.screen_roomlist_filter_people_empty_state_title,
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
)
RoomListFilter.Rooms -> RoomListFiltersEmptyStateResources(
title = R.string.screen_roomlist_filter_rooms_empty_state_title,
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
)
RoomListFilter.Favourites -> RoomListFiltersEmptyStateResources(
title = R.string.screen_roomlist_filter_favourites_empty_state_title,
subtitle = R.string.screen_roomlist_filter_favourites_empty_state_subtitle
)
RoomListFilter.Invites -> RoomListFiltersEmptyStateResources(
title = R.string.screen_roomlist_filter_invites_empty_state_title,
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
)
}
}
else -> RoomListFiltersEmptyStateResources(
title = R.string.screen_roomlist_filter_mixed_empty_state_title,
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
)
}
}
}
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters
sealed interface RoomListFiltersEvents {
data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvents
data object ClearSelectedFilters : RoomListFiltersEvents
}
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.map
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter
@Inject
class RoomListFiltersPresenter(
private val roomListService: RoomListService,
private val filterSelectionStrategy: FilterSelectionStrategy,
) : Presenter<RoomListFiltersState> {
private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList()
@Composable
override fun present(): RoomListFiltersState {
fun handleEvent(event: RoomListFiltersEvents) {
when (event) {
RoomListFiltersEvents.ClearSelectedFilters -> {
filterSelectionStrategy.clear()
}
is RoomListFiltersEvents.ToggleFilter -> {
filterSelectionStrategy.toggle(event.filter)
}
}
}
val filters by produceState(initialValue = initialFilters) {
filterSelectionStrategy.filterSelectionStates
.map { filters ->
value = filters.toImmutableList()
filters.mapNotNull { filterState ->
if (!filterState.isSelected) {
return@mapNotNull null
}
when (filterState.filter) {
RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group
RoomListFilter.People -> MatrixRoomListFilter.Category.People
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite
RoomListFilter.Invites -> MatrixRoomListFilter.Invite
}
}
}
.collect { filters ->
val result = MatrixRoomListFilter.All(filters)
roomListService.allRooms.updateFilter(result)
}
}
return RoomListFiltersState(
filterSelectionStates = filters,
eventSink = ::handleEvent,
)
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
data class RoomListFiltersState(
val filterSelectionStates: ImmutableList<FilterSelectionState>,
val eventSink: (RoomListFiltersEvents) -> Unit,
) {
val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected }
fun selectedFilters(): ImmutableList<RoomListFilter> {
return filterSelectionStates
.filter { it.isSelected }
.map { it.filter }
.toImmutableList()
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import kotlinx.collections.immutable.toImmutableList
class RoomListFiltersStateProvider : PreviewParameterProvider<RoomListFiltersState> {
override val values: Sequence<RoomListFiltersState>
get() = sequenceOf(
aRoomListFiltersState(),
aRoomListFiltersState(
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
),
)
}
fun aRoomListFiltersState(
filterSelectionStates: List<FilterSelectionState> = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = false) },
eventSink: (RoomListFiltersEvents) -> Unit = {},
) = RoomListFiltersState(
filterSelectionStates = filterSelectionStates.toImmutableList(),
eventSink = eventSink,
)
@@ -0,0 +1,220 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
/**
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2191-606
*/
@Composable
fun RoomListFiltersView(
state: RoomListFiltersState,
modifier: Modifier = Modifier
) {
fun onClearFiltersClick() {
state.eventSink(RoomListFiltersEvents.ClearSelectedFilters)
}
fun onToggleFilter(filter: RoomListFilter) {
state.eventSink(RoomListFiltersEvents.ToggleFilter(filter))
}
var scrollToStart by remember { mutableIntStateOf(0) }
val lazyListState = rememberLazyListState()
LaunchedEffect(scrollToStart) {
// Scroll until the first item start to be displayed
// Since all items have different size, there is no way to compute the amount of
// pixel to scroll to go directly to the start of the row.
// But IRL it should only happen for one item.
while (lazyListState.firstVisibleItemIndex > 0) {
lazyListState.animateScrollBy(
value = -(lazyListState.firstVisibleItemScrollOffset + 1f),
animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
)
)
}
// Then scroll to the start of the list, a bit faster, to fully reveal the first
// item, which can be the close button to reset filter, or the first item
// if the user has scroll a bit before clicking on the close button.
lazyListState.animateScrollBy(
value = -lazyListState.firstVisibleItemScrollOffset.toFloat(),
animationSpec = spring(
stiffness = Spring.StiffnessMedium,
)
)
}
val previousFilters = remember { mutableStateOf(listOf<RoomListFilter>()) }
LazyRow(
contentPadding = PaddingValues(start = 8.dp, end = 16.dp),
modifier = modifier.fillMaxWidth(),
state = lazyListState,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
item("clear_filters") {
if (state.hasAnyFilterSelected) {
RoomListClearFiltersButton(
modifier = Modifier
.padding(start = 8.dp)
.testTag(TestTags.homeScreenClearFilters),
onClick = {
previousFilters.value = state.selectedFilters()
onClearFiltersClick()
// When clearing filter, we want to ensure that the list
// of filters is scrolled to the start.
scrollToStart++
}
)
}
}
state.filterSelectionStates.forEachIndexed { i, filterWithSelection ->
item(filterWithSelection.filter) {
val zIndex = (if (previousFilters.value.contains(filterWithSelection.filter)) state.filterSelectionStates.size else 0) - i.toFloat()
RoomListFilterView(
modifier = Modifier
.animateItem()
.zIndex(zIndex),
roomListFilter = filterWithSelection.filter,
selected = filterWithSelection.isSelected,
onClick = {
previousFilters.value = state.selectedFilters()
onToggleFilter(it)
// When selecting a filter, we want to scroll to the start of the list
if (filterWithSelection.isSelected.not()) {
scrollToStart++
}
},
)
}
}
}
}
@Composable
private fun RoomListClearFiltersButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.clip(CircleShape)
.background(ElementTheme.colors.bgActionPrimaryRest)
.clickable(onClick = onClick)
.padding(4.dp)
) {
Icon(
modifier = Modifier
.align(Alignment.Center)
.size(16.dp),
imageVector = CompoundIcons.Close(),
tint = ElementTheme.colors.iconOnSolidPrimary,
contentDescription = stringResource(id = R.string.screen_roomlist_clear_filters),
)
}
}
@Composable
private fun RoomListFilterView(
roomListFilter: RoomListFilter,
selected: Boolean,
onClick: (RoomListFilter) -> Unit,
modifier: Modifier = Modifier
) {
val background = animateColorAsState(
targetValue = if (selected) ElementTheme.colors.bgActionPrimaryRest else ElementTheme.colors.bgCanvasDefault,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "chip background colour",
)
val textColour = animateColorAsState(
targetValue = if (selected) ElementTheme.colors.textOnSolidPrimary else ElementTheme.colors.textPrimary,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "chip text colour",
)
val borderColour = animateColorAsState(
targetValue = if (selected) Color.Transparent else ElementTheme.colors.borderInteractiveSecondary,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "chip border colour",
)
FilterChip(
selected = selected,
onClick = { onClick(roomListFilter) },
modifier = modifier.height(32.dp),
shape = CircleShape,
colors = FilterChipDefaults.filterChipColors(
containerColor = background.value,
selectedContainerColor = background.value,
labelColor = textColour.value,
selectedLabelColor = textColour.value,
),
label = {
Text(
text = stringResource(id = roomListFilter.stringResource),
style = ElementTheme.typography.fontBodyMdRegular,
)
},
border = FilterChipDefaults.filterChipBorder(
enabled = true,
selected = selected,
borderColor = borderColour.value,
),
)
}
@PreviewsDayNight
@Composable
internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStateProvider::class) state: RoomListFiltersState) = ElementPreview {
RoomListFiltersView(
modifier = Modifier.padding(vertical = 4.dp),
state = state,
)
}
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters.selection
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.home.impl.filters.RoomListFilter
import io.element.android.libraries.di.SessionScope
import kotlinx.coroutines.flow.MutableStateFlow
@ContributesBinding(SessionScope::class)
class DefaultFilterSelectionStrategy : FilterSelectionStrategy {
private val selectedFilters = LinkedHashSet<RoomListFilter>()
override val filterSelectionStates = MutableStateFlow(buildFilters())
override fun select(filter: RoomListFilter) {
selectedFilters.add(filter)
filterSelectionStates.value = buildFilters()
}
override fun deselect(filter: RoomListFilter) {
selectedFilters.remove(filter)
filterSelectionStates.value = buildFilters()
}
override fun isSelected(filter: RoomListFilter): Boolean {
return selectedFilters.contains(filter)
}
override fun clear() {
selectedFilters.clear()
filterSelectionStates.value = buildFilters()
}
private fun buildFilters(): Set<FilterSelectionState> {
val selectedFilterStates = selectedFilters.map {
FilterSelectionState(
filter = it,
isSelected = true
)
}
val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet()
val unselectedFilterStates = unselectedFilters.map {
FilterSelectionState(
filter = it,
isSelected = false
)
}
return (selectedFilterStates + unselectedFilterStates).toSet()
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters.selection
import io.element.android.features.home.impl.filters.RoomListFilter
data class FilterSelectionState(
val filter: RoomListFilter,
val isSelected: Boolean,
)
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.filters.selection
import io.element.android.features.home.impl.filters.RoomListFilter
import kotlinx.coroutines.flow.StateFlow
interface FilterSelectionStrategy {
val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
fun select(filter: RoomListFilter)
fun deselect(filter: RoomListFilter)
fun isSelected(filter: RoomListFilter): Boolean
fun clear()
fun toggle(filter: RoomListFilter) {
if (isSelected(filter)) {
deselect(filter)
} else {
select(filter)
}
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.model
import androidx.compose.runtime.Immutable
@Immutable
sealed interface LatestEvent {
data object None : LatestEvent
data class Synced(
val content: CharSequence?,
) : LatestEvent
data class Sending(
val content: CharSequence?,
) : LatestEvent
data object Error : LatestEvent
fun content(): CharSequence? {
return when (this) {
is None -> null
is Synced -> content
is Sending -> content
is Error -> null
}
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.model
import androidx.compose.runtime.Immutable
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class RoomListRoomSummary(
val id: String,
val displayType: RoomSummaryDisplayType,
val roomId: RoomId,
val name: String?,
val canonicalAlias: RoomAlias?,
val numberOfUnreadMessages: Long,
val numberOfUnreadMentions: Long,
val numberOfUnreadNotifications: Long,
val isMarkedUnread: Boolean,
val timestamp: String?,
val latestEvent: LatestEvent,
val avatarData: AvatarData,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val isDirect: Boolean,
val isDm: Boolean,
val isFavorite: Boolean,
val inviteSender: InviteSender?,
val isTombstoned: Boolean,
val heroes: ImmutableList<AvatarData>,
val isSpace: Boolean,
) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
isMarkedUnread
val hasNewContent = numberOfUnreadMessages > 0 ||
numberOfUnreadMentions > 0 ||
numberOfUnreadNotifications > 0 ||
isMarkedUnread
fun toInviteData() = InviteData(
roomId = roomId,
roomName = name ?: roomId.value,
isDm = isDm,
)
}
@@ -0,0 +1,193 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toImmutableList
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
override val values: Sequence<RoomListRoomSummary>
get() = sequenceOf(
listOf(
aRoomListRoomSummary(displayType = RoomSummaryDisplayType.PLACEHOLDER),
aRoomListRoomSummary(),
aRoomListRoomSummary(name = null),
aRoomListRoomSummary(latestEvent = LatestEvent.None),
aRoomListRoomSummary(
name = "A very long room name that should be truncated",
latestEvent = LatestEvent.Synced(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" +
" ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" +
"modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
),
timestamp = "yesterday",
numberOfUnreadMessages = 1,
),
),
listOf(false, true).map { hasCall ->
listOf(
RoomNotificationMode.ALL_MESSAGES,
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
RoomNotificationMode.MUTE,
).map { roomNotificationMode ->
listOf(
aRoomListRoomSummary(
name = roomNotificationMode.name,
latestEvent = LatestEvent.Synced("No activity" + if (hasCall) ", call" else ""),
notificationMode = roomNotificationMode,
numberOfUnreadMessages = 0,
numberOfUnreadMentions = 0,
hasRoomCall = hasCall,
),
aRoomListRoomSummary(
name = roomNotificationMode.name,
latestEvent = LatestEvent.Synced("New messages" + if (hasCall) ", call" else ""),
notificationMode = roomNotificationMode,
numberOfUnreadMessages = 1,
numberOfUnreadMentions = 0,
hasRoomCall = hasCall,
),
aRoomListRoomSummary(
name = roomNotificationMode.name,
latestEvent = LatestEvent.Synced("New messages, mentions" + if (hasCall) ", call" else ""),
notificationMode = roomNotificationMode,
numberOfUnreadMessages = 1,
numberOfUnreadMentions = 1,
hasRoomCall = hasCall,
),
aRoomListRoomSummary(
name = roomNotificationMode.name,
latestEvent = LatestEvent.Synced("New mentions" + if (hasCall) ", call" else ""),
notificationMode = roomNotificationMode,
numberOfUnreadMessages = 0,
numberOfUnreadMentions = 1,
hasRoomCall = hasCall,
),
)
}.flatten()
}.flatten(),
listOf(
aRoomListRoomSummary(
displayType = RoomSummaryDisplayType.INVITE,
inviteSender = anInviteSender(
userId = UserId("@alice:matrix.org"),
displayName = "Alice",
),
canonicalAlias = RoomAlias("#alias:matrix.org"),
),
aRoomListRoomSummary(
name = "Bob",
displayType = RoomSummaryDisplayType.INVITE,
inviteSender = anInviteSender(
userId = UserId("@bob:matrix.org"),
displayName = "Bob",
),
isDm = true,
),
aRoomListRoomSummary(
name = null,
displayType = RoomSummaryDisplayType.INVITE,
inviteSender = anInviteSender(
userId = UserId("@bob:matrix.org"),
displayName = "Bob",
),
),
aRoomListRoomSummary(
name = "A space invite",
displayType = RoomSummaryDisplayType.INVITE,
inviteSender = anInviteSender(
userId = UserId("@bob:matrix.org"),
displayName = "Bob",
),
isSpace = true
),
aRoomListRoomSummary(
name = "A knocked room",
displayType = RoomSummaryDisplayType.KNOCKED,
),
aRoomListRoomSummary(
name = "A knocked room with alias",
canonicalAlias = RoomAlias("#knockable:matrix.org"),
displayType = RoomSummaryDisplayType.KNOCKED,
),
aRoomListRoomSummary(
name = "A tombstoned room",
displayType = RoomSummaryDisplayType.ROOM,
isTombstoned = true,
)
),
listOf(
aRoomListRoomSummary(latestEvent = LatestEvent.Sending("A sending message")),
aRoomListRoomSummary(latestEvent = LatestEvent.Error),
)
).flatten()
}
internal fun anInviteSender(
userId: UserId = UserId("@bob:domain"),
displayName: String = "Bob",
avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
) = InviteSender(
userId = userId,
displayName = displayName,
avatarData = avatarData,
membershipChangeReason = null,
)
internal fun aRoomListRoomSummary(
id: String = "!roomId:domain",
name: String? = "Room name",
numberOfUnreadMessages: Long = 0,
numberOfUnreadMentions: Long = 0,
numberOfUnreadNotifications: Long = 0,
isMarkedUnread: Boolean = false,
latestEvent: LatestEvent = LatestEvent.Synced("Last message"),
timestamp: String? = latestEvent.takeIf { it !is LatestEvent.None }?.let { "88:88" },
notificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
isDirect: Boolean = false,
isDm: Boolean = false,
isFavorite: Boolean = false,
inviteSender: InviteSender? = null,
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
canonicalAlias: RoomAlias? = null,
heroes: List<AvatarData> = emptyList(),
isTombstoned: Boolean = false,
isSpace: Boolean = false,
) = RoomListRoomSummary(
id = id,
roomId = RoomId(id),
name = name,
numberOfUnreadMessages = numberOfUnreadMessages,
numberOfUnreadMentions = numberOfUnreadMentions,
numberOfUnreadNotifications = numberOfUnreadNotifications,
isMarkedUnread = isMarkedUnread,
timestamp = timestamp,
latestEvent = latestEvent,
avatarData = avatarData,
userDefinedNotificationMode = notificationMode,
hasRoomCall = hasRoomCall,
isDirect = isDirect,
isDm = isDm,
isFavorite = isFavorite,
inviteSender = inviteSender,
displayType = displayType,
canonicalAlias = canonicalAlias,
heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned,
isSpace = isSpace
)
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.model
/**
* Represents the type of display for a room list item.
*/
enum class RoomSummaryDisplayType {
PLACEHOLDER,
ROOM,
INVITE,
KNOCKED,
}
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.roomlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableSet
open class RoomListContentStateProvider : PreviewParameterProvider<RoomListContentState> {
override val values: Sequence<RoomListContentState>
get() = sequenceOf(
aRoomsContentState(),
aRoomsContentState(summaries = persistentListOf()),
aSkeletonContentState(),
anEmptyContentState(),
anEmptyContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
aRoomsContentState(
showNewNotificationSoundBanner = true,
),
)
}
internal fun aRoomsContentState(
securityBannerState: SecurityBannerState = SecurityBannerState.None,
showNewNotificationSoundBanner: Boolean = false,
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
seenRoomInvites: Set<RoomId> = emptySet(),
) = RoomListContentState.Rooms(
securityBannerState = securityBannerState,
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
batteryOptimizationState = batteryOptimizationState,
summaries = summaries,
seenRoomInvites = seenRoomInvites.toImmutableSet(),
)
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
internal fun anEmptyContentState(
securityBannerState: SecurityBannerState = SecurityBannerState.None,
) = RoomListContentState.Empty(
securityBannerState = securityBannerState,
)
@@ -0,0 +1,229 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.roomlist
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomListContextMenu(
contextMenu: RoomListState.ContextMenu.Shown,
canReportRoom: Boolean,
eventSink: (RoomListEvents.ContextMenuEvents) -> Unit,
onRoomSettingsClick: (roomId: RoomId) -> Unit,
onReportRoomClick: (roomId: RoomId) -> Unit
) {
ModalBottomSheet(
onDismissRequest = { eventSink(RoomListEvents.HideContextMenu) },
) {
RoomListModalBottomSheetContent(
contextMenu = contextMenu,
canReportRoom = canReportRoom,
onRoomMarkReadClick = {
eventSink(RoomListEvents.HideContextMenu)
eventSink(RoomListEvents.MarkAsRead(contextMenu.roomId))
},
onRoomMarkUnreadClick = {
eventSink(RoomListEvents.HideContextMenu)
eventSink(RoomListEvents.MarkAsUnread(contextMenu.roomId))
},
onRoomSettingsClick = {
eventSink(RoomListEvents.HideContextMenu)
onRoomSettingsClick(contextMenu.roomId)
},
onLeaveRoomClick = {
eventSink(RoomListEvents.HideContextMenu)
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId, needsConfirmation = true))
},
onFavoriteChange = { isFavorite ->
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
},
onClearCacheRoomClick = {
eventSink(RoomListEvents.HideContextMenu)
eventSink(RoomListEvents.ClearCacheOfRoom(contextMenu.roomId))
},
onReportRoomClick = {
eventSink(RoomListEvents.HideContextMenu)
onReportRoomClick(contextMenu.roomId)
},
)
}
}
@Composable
private fun RoomListModalBottomSheetContent(
contextMenu: RoomListState.ContextMenu.Shown,
canReportRoom: Boolean,
onRoomSettingsClick: () -> Unit,
onLeaveRoomClick: () -> Unit,
onFavoriteChange: (isFavorite: Boolean) -> Unit,
onRoomMarkReadClick: () -> Unit,
onRoomMarkUnreadClick: () -> Unit,
onClearCacheRoomClick: () -> Unit,
onReportRoomClick: () -> Unit,
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
ListItem(
headlineContent = {
Text(
text = contextMenu.roomName ?: stringResource(id = CommonStrings.common_no_room_name),
style = ElementTheme.typography.fontBodyLgMedium,
fontStyle = FontStyle.Italic.takeIf { contextMenu.roomName == null }
)
}
)
if (contextMenu.hasNewContent) {
ListItem(
headlineContent = {
Text(
text = stringResource(id = R.string.screen_roomlist_mark_as_read),
style = MaterialTheme.typography.bodyLarge,
)
},
onClick = onRoomMarkReadClick,
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(CompoundIcons.MarkAsRead())
),
style = ListItemStyle.Primary,
)
} else {
ListItem(
headlineContent = {
Text(
text = stringResource(id = R.string.screen_roomlist_mark_as_unread),
style = MaterialTheme.typography.bodyLarge,
)
},
onClick = onRoomMarkUnreadClick,
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(CompoundIcons.MarkAsUnread())
),
style = ListItemStyle.Primary,
)
}
ListItem(
headlineContent = {
Text(
text = stringResource(id = CommonStrings.common_favourite),
style = MaterialTheme.typography.bodyLarge,
)
},
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Favourite(),
)
),
trailingContent = ListItemContent.Switch(
checked = contextMenu.isFavorite,
),
onClick = {
onFavoriteChange(!contextMenu.isFavorite)
},
style = ListItemStyle.Primary,
)
ListItem(
headlineContent = {
Text(
text = stringResource(id = CommonStrings.common_settings),
style = MaterialTheme.typography.bodyLarge,
)
},
modifier = Modifier.clickable { onRoomSettingsClick() },
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Settings(),
)
),
style = ListItemStyle.Primary,
)
if (canReportRoom) {
ListItem(
headlineContent = {
Text(text = stringResource(CommonStrings.action_report_room))
},
modifier = Modifier.clickable { onReportRoomClick() },
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.ChatProblem(),
)
),
style = ListItemStyle.Destructive,
)
}
ListItem(
headlineContent = {
Text(text = stringResource(CommonStrings.action_leave_room))
},
modifier = Modifier.clickable { onLeaveRoomClick() },
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(
CompoundIcons.Leave(),
)
),
style = ListItemStyle.Destructive,
)
if (contextMenu.displayClearRoomCacheAction) {
ListItem(
headlineContent = {
Text(text = "Clear cache for this room")
},
modifier = Modifier.clickable { onClearCacheRoomClick() },
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(CompoundIcons.Delete())
),
style = ListItemStyle.Primary,
)
}
}
}
// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up.
// see: https://issuetracker.google.com/issues/283843380
// Remove this preview when the issue is fixed.
@PreviewsDayNight
@Composable
internal fun RoomListModalBottomSheetContentPreview(
@PreviewParameter(RoomListStateContextMenuShownProvider::class) contextMenu: RoomListState.ContextMenu.Shown
) = ElementPreview {
RoomListModalBottomSheetContent(
contextMenu = contextMenu,
canReportRoom = true,
onRoomMarkReadClick = {},
onRoomMarkUnreadClick = {},
onRoomSettingsClick = {},
onLeaveRoomClick = {},
onFavoriteChange = {},
onClearCacheRoomClick = {},
onReportRoomClick = {},
)
}
@@ -0,0 +1,127 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.roomlist
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomListDeclineInviteMenu(
menu: RoomListState.DeclineInviteMenu.Shown,
canReportRoom: Boolean,
onDeclineAndBlockClick: (RoomListRoomSummary) -> Unit,
eventSink: (RoomListEvents) -> Unit,
) {
ModalBottomSheet(
onDismissRequest = { eventSink(RoomListEvents.HideDeclineInviteMenu) },
) {
RoomListDeclineInviteMenuContent(
roomName = menu.roomSummary.name ?: menu.roomSummary.roomId.value,
onDeclineClick = {
eventSink(RoomListEvents.HideDeclineInviteMenu)
eventSink(RoomListEvents.DeclineInvite(menu.roomSummary, false))
},
onDeclineAndBlockClick = {
eventSink(RoomListEvents.HideDeclineInviteMenu)
if (canReportRoom) {
onDeclineAndBlockClick(menu.roomSummary)
} else {
eventSink(RoomListEvents.DeclineInvite(menu.roomSummary, true))
}
},
onCancelClick = {
eventSink(RoomListEvents.HideDeclineInviteMenu)
}
)
}
}
@Composable
private fun RoomListDeclineInviteMenuContent(
roomName: String,
onDeclineClick: () -> Unit,
onDeclineAndBlockClick: () -> Unit,
onCancelClick: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(R.string.screen_invites_decline_chat_title),
style = ElementTheme.typography.fontHeadingSmMedium,
color = ElementTheme.colors.textPrimary,
)
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(R.string.screen_invites_decline_chat_message, roomName),
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(Modifier.height(22.dp))
Button(
text = stringResource(CommonStrings.action_decline),
modifier = Modifier.fillMaxWidth(),
onClick = onDeclineClick,
)
Spacer(Modifier.height(16.dp))
OutlinedButton(
text = stringResource(CommonStrings.action_decline_and_block),
modifier = Modifier.fillMaxWidth(),
destructive = true,
onClick = onDeclineAndBlockClick
)
Spacer(Modifier.height(16.dp))
TextButton(
text = stringResource(CommonStrings.action_cancel),
modifier = Modifier.fillMaxWidth(),
onClick = onCancelClick
)
}
}
// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up.
// see: https://issuetracker.google.com/issues/283843380
// Remove this preview when the issue is fixed.
@PreviewsDayNight
@Composable
internal fun RoomListDeclineInviteMenuContentPreview() = ElementPreview {
RoomListDeclineInviteMenuContent(
roomName = "Room name",
onCancelClick = {},
onDeclineClick = {},
onDeclineAndBlockClick = {},
)
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.roomlist
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface RoomListEvents {
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
data object DismissRequestVerificationPrompt : RoomListEvents
data object DismissBanner : RoomListEvents
data object DismissNewNotificationSoundBanner : RoomListEvents
data object ToggleSearchResults : RoomListEvents
data class ShowContextMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents
data class AcceptInvite(val roomSummary: RoomListRoomSummary) : RoomListEvents
data class DeclineInvite(val roomSummary: RoomListRoomSummary, val blockUser: Boolean) : RoomListEvents
data class ShowDeclineInviteMenu(val roomSummary: RoomListRoomSummary) : RoomListEvents
data object HideDeclineInviteMenu : RoomListEvents
sealed interface ContextMenuEvents : RoomListEvents
data object HideContextMenu : ContextMenuEvents
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : 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
data class ClearCacheOfRoom(val roomId: RoomId) : ContextMenuEvents
}
@@ -0,0 +1,344 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.roomlist
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
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.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.search.RoomListSearchEvents
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
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
private const val EXTENDED_RANGE_SIZE = 40
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
@Inject
class RoomListPresenter(
private val client: MatrixClient,
private val leaveRoomPresenter: Presenter<LeaveRoomState>,
private val roomListDataSource: RoomListDataSource,
private val filtersPresenter: Presenter<RoomListFiltersState>,
private val searchPresenter: Presenter<RoomListSearchState>,
private val sessionPreferencesStore: SessionPreferencesStore,
private val analyticsService: AnalyticsService,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
private val batteryOptimizationPresenter: Presenter<BatteryOptimizationState>,
private val notificationCleaner: NotificationCleaner,
private val appPreferencesStore: AppPreferencesStore,
private val seenInvitesStore: SeenInvitesStore,
private val announcementService: AnnouncementService,
private val coldStartWatcher: AnalyticsColdStartWatcher,
) : Presenter<RoomListState> {
private val encryptionService = client.encryptionService
@Composable
override fun present(): RoomListState {
val coroutineScope = rememberCoroutineScope()
val leaveRoomState = leaveRoomPresenter.present()
val filtersState = filtersPresenter.present()
val searchState = searchPresenter.present()
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
LaunchedEffect(Unit) {
roomListDataSource.launchIn(this)
}
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
val showNewNotificationSoundBanner by remember {
announcementService.announcementsToShowFlow().map { announcements ->
announcements.contains(Announcement.NewNotificationSound)
}
}.collectAsState(false)
// Avatar indicator
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
val declineInviteMenu = remember { mutableStateOf<RoomListState.DeclineInviteMenu>(RoomListState.DeclineInviteMenu.Hidden) }
fun handleEvent(event: RoomListEvents) {
when (event) {
is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch {
updateVisibleRange(event.range)
}
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
RoomListEvents.DismissBanner -> securityBannerDismissed = true
RoomListEvents.DismissNewNotificationSoundBanner -> coroutineScope.launch {
announcementService.onAnnouncementDismissed(Announcement.NewNotificationSound)
}
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
is RoomListEvents.ShowContextMenu -> {
coroutineScope.showContextMenu(event, contextMenu)
}
is RoomListEvents.HideContextMenu -> {
contextMenu.value = RoomListState.ContextMenu.Hidden
}
is RoomListEvents.LeaveRoom -> {
leaveRoomState.eventSink(LeaveRoomEvent.LeaveRoom(event.roomId, needsConfirmation = event.needsConfirmation))
}
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite)
is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId)
is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId)
is RoomListEvents.AcceptInvite -> {
acceptDeclineInviteState.eventSink(
AcceptInvite(event.roomSummary.toInviteData())
)
}
is RoomListEvents.DeclineInvite -> {
acceptDeclineInviteState.eventSink(
DeclineInvite(event.roomSummary.toInviteData(), blockUser = event.blockUser, shouldConfirm = false)
)
}
is RoomListEvents.ShowDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Shown(event.roomSummary)
RoomListEvents.HideDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Hidden
is RoomListEvents.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId)
}
}
val contentState = roomListContentState(
securityBannerDismissed,
showNewNotificationSoundBanner,
)
val canReportRoom by produceState(false) { value = client.canReportRoom() }
return RoomListState(
contextMenu = contextMenu.value,
declineInviteMenu = declineInviteMenu.value,
leaveRoomState = leaveRoomState,
filtersState = filtersState,
searchState = searchState,
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
hideInvitesAvatars = hideInvitesAvatar,
canReportRoom = canReportRoom,
eventSink = ::handleEvent,
)
}
@Composable
private fun rememberSecurityBannerState(
securityBannerDismissed: Boolean,
): State<SecurityBannerState> {
val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
return remember {
derivedStateOf {
calculateBannerState(
securityBannerDismissed = currentSecurityBannerDismissed,
recoveryState = recoveryState,
)
}
}
}
private fun calculateBannerState(
securityBannerDismissed: Boolean,
recoveryState: RecoveryState,
): SecurityBannerState {
if (securityBannerDismissed) {
return SecurityBannerState.None
}
when (recoveryState) {
RecoveryState.DISABLED -> return SecurityBannerState.SetUpRecovery
RecoveryState.INCOMPLETE -> return SecurityBannerState.RecoveryKeyConfirmation
RecoveryState.UNKNOWN,
RecoveryState.WAITING_FOR_SYNC,
RecoveryState.ENABLED -> Unit
}
return SecurityBannerState.None
}
@Composable
private fun roomListContentState(
securityBannerDismissed: Boolean,
showNewNotificationSoundBanner: Boolean,
): RoomListContentState {
val roomSummaries by produceState(initialValue = AsyncData.Loading()) {
roomListDataSource.allRooms.collect { value = AsyncData.Success(it) }
}
val loadingState by roomListDataSource.loadingState.collectAsState()
val showEmpty by remember {
derivedStateOf {
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
}
}
val showSkeleton by remember {
derivedStateOf {
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
}
}
val seenRoomInvites by remember { seenInvitesStore.seenRoomIds() }.collectAsState(emptySet())
val securityBannerState by rememberSecurityBannerState(securityBannerDismissed)
return when {
showEmpty -> RoomListContentState.Empty(
securityBannerState = securityBannerState,
)
showSkeleton -> RoomListContentState.Skeleton(count = 16)
else -> {
coldStartWatcher.onRoomListVisible()
RoomListContentState.Rooms(
securityBannerState = securityBannerState,
showNewNotificationSoundBanner = showNewNotificationSoundBanner,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
batteryOptimizationState = batteryOptimizationPresenter.present(),
summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(),
seenRoomInvites = seenRoomInvites.toImmutableSet(),
)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState<RoomListState.ContextMenu>) = launch {
val initialState = RoomListState.ContextMenu.Shown(
roomId = event.roomSummary.roomId,
roomName = event.roomSummary.name,
isDm = event.roomSummary.isDm,
isFavorite = event.roomSummary.isFavorite,
hasNewContent = event.roomSummary.hasNewContent,
displayClearRoomCacheAction = appPreferencesStore.isDeveloperModeEnabledFlow().first(),
)
contextMenuState.value = initialState
client.getRoom(event.roomSummary.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 CoroutineScope.setRoomIsFavorite(roomId: RoomId, isFavorite: Boolean) = launch {
client.getRoom(roomId)?.use { room ->
room.setIsFavorite(isFavorite)
.onSuccess {
analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuFavouriteToggle)
}
}
}
private fun CoroutineScope.markAsRead(roomId: RoomId) = launch {
notificationCleaner.clearMessagesForRoom(client.sessionId, roomId)
client.getRoom(roomId)?.use { room ->
room.setUnreadFlag(isUnread = false)
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
ReceiptType.READ
} else {
ReceiptType.READ_PRIVATE
}
room.markAsRead(receiptType)
.onSuccess {
analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle)
}
}
}
private fun CoroutineScope.markAsUnread(roomId: RoomId) = launch {
client.getRoom(roomId)?.use { room ->
room.setUnreadFlag(isUnread = true)
.onSuccess {
analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle)
}
}
}
private fun CoroutineScope.clearCacheOfRoom(roomId: RoomId) = launch {
client.getRoom(roomId)?.use { room ->
room.clearEventCacheStorage()
}
}
private var currentUpdateVisibleRangeJob: Job? = null
private fun CoroutineScope.updateVisibleRange(range: IntRange) {
currentUpdateVisibleRangeJob?.cancel()
currentUpdateVisibleRangeJob = launch {
// Debounce the subscription to avoid subscribing to too many rooms
delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS)
if (range.isEmpty()) return@launch
val currentRoomList = roomListDataSource.allRooms.first()
// Use extended range to 'prefetch' the next rooms info
val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2
val extendedRange = range.first until range.last + midExtendedRangeSize
val roomIds = extendedRange.mapNotNull { index ->
currentRoomList.getOrNull(index)?.roomId
}
roomListDataSource.subscribeToVisibleRooms(roomIds)
}
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.roomlist
import androidx.compose.runtime.Immutable
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
data class RoomListState(
val contextMenu: ContextMenu,
val declineInviteMenu: DeclineInviteMenu,
val leaveRoomState: LeaveRoomState,
val filtersState: RoomListFiltersState,
val searchState: RoomListSearchState,
val contentState: RoomListContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState,
val hideInvitesAvatars: Boolean,
val canReportRoom: Boolean,
val eventSink: (RoomListEvents) -> Unit,
) {
val displayFilters = contentState is RoomListContentState.Rooms
sealed interface ContextMenu {
data object Hidden : ContextMenu
data class Shown(
val roomId: RoomId,
val roomName: String?,
val isDm: Boolean,
val isFavorite: Boolean,
val hasNewContent: Boolean,
val displayClearRoomCacheAction: Boolean,
) : ContextMenu
}
sealed interface DeclineInviteMenu {
data object Hidden : DeclineInviteMenu
data class Shown(val roomSummary: RoomListRoomSummary) : DeclineInviteMenu
}
}
enum class SecurityBannerState {
None,
SetUpRecovery,
RecoveryKeyConfirmation,
}
@Immutable
sealed interface RoomListContentState {
data class Skeleton(val count: Int) : RoomListContentState
data class Empty(
val securityBannerState: SecurityBannerState,
) : RoomListContentState
data class Rooms(
val securityBannerState: SecurityBannerState,
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
val batteryOptimizationState: BatteryOptimizationState,
val showNewNotificationSoundBanner: Boolean,
val summaries: ImmutableList<RoomListRoomSummary>,
val seenRoomInvites: ImmutableSet<RoomId>,
) : RoomListContentState
}
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.roomlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
open class RoomListStateContextMenuShownProvider : PreviewParameterProvider<RoomListState.ContextMenu.Shown> {
override val values: Sequence<RoomListState.ContextMenu.Shown>
get() = sequenceOf(
aContextMenuShown(hasNewContent = true),
aContextMenuShown(isDm = true),
aContextMenuShown(roomName = null)
)
}
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,
hasNewContent = hasNewContent,
isFavorite = isFavorite,
displayClearRoomCacheAction = false,
)
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.roomlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.aRoomListFiltersState
import io.element.android.features.home.impl.model.LatestEvent
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.model.aRoomListRoomSummary
import io.element.android.features.home.impl.model.anInviteSender
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.search.aRoomListSearchState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
override val values: Sequence<RoomListState>
get() = sequenceOf(
aRoomListState(),
aRoomListState(contextMenu = aContextMenuShown(roomName = null)),
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
aRoomListState(contentState = anEmptyContentState()),
aRoomListState(contentState = aSkeletonContentState()),
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
aRoomListState(contentState = aRoomsContentState(batteryOptimizationState = aBatteryOptimizationState(shouldDisplayBanner = true))),
aRoomListState(contentState = anEmptyContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
)
}
internal fun aRoomListState(
contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden,
declineInviteMenu: RoomListState.DeclineInviteMenu = RoomListState.DeclineInviteMenu.Hidden,
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
searchState: RoomListSearchState = aRoomListSearchState(),
filtersState: RoomListFiltersState = aRoomListFiltersState(),
contentState: RoomListContentState = aRoomsContentState(),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
hideInvitesAvatars: Boolean = false,
canReportRoom: Boolean = true,
eventSink: (RoomListEvents) -> Unit = {}
) = RoomListState(
contextMenu = contextMenu,
declineInviteMenu = declineInviteMenu,
leaveRoomState = leaveRoomState,
filtersState = filtersState,
searchState = searchState,
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
hideInvitesAvatars = hideInvitesAvatars,
canReportRoom = canReportRoom,
eventSink = eventSink,
)
internal fun aLeaveRoomState(
eventSink: (LeaveRoomEvent) -> Unit = {}
) = object : LeaveRoomState {
override val eventSink: (LeaveRoomEvent) -> Unit = eventSink
}
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
return persistentListOf(
aRoomListRoomSummary(
name = "Room Invited",
avatarData = AvatarData("!roomId", "Room with Alice and Bob", size = AvatarSize.RoomListItem),
id = "!roomId:domain",
inviteSender = anInviteSender(),
displayType = RoomSummaryDisplayType.INVITE,
),
aRoomListRoomSummary(
name = "Room",
numberOfUnreadMessages = 1,
timestamp = "14:18",
latestEvent = LatestEvent.Synced("A very very very very long message which suites on two lines"),
avatarData = AvatarData("!id", "R", size = AvatarSize.RoomListItem),
id = "!roomId:domain",
),
aRoomListRoomSummary(
name = "Room#2",
numberOfUnreadMessages = 0,
timestamp = "14:16",
latestEvent = LatestEvent.Synced("A short message"),
avatarData = AvatarData("!id", "Z", size = AvatarSize.RoomListItem),
id = "!roomId2:domain",
),
aRoomListRoomSummary(
id = "!roomId3:domain",
displayType = RoomSummaryDisplayType.PLACEHOLDER,
),
aRoomListRoomSummary(
id = "!roomId4:domain",
displayType = RoomSummaryDisplayType.PLACEHOLDER,
),
)
}
internal fun generateRoomListRoomSummaryList(
numberOfRooms: Int = 10,
): ImmutableList<RoomListRoomSummary> {
return List(numberOfRooms) { index ->
aRoomListRoomSummary(
name = "Room#$index",
numberOfUnreadMessages = 0,
timestamp = "14:16",
latestEvent = LatestEvent.Synced("A message"),
avatarData = AvatarData("!id$index", "${(65 + index % 26).toChar()}", size = AvatarSize.RoomListItem),
id = "!roomId$index:domain",
)
}.toImmutableList()
}
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.search
import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
private const val PAGE_SIZE = 30
@Inject
class RoomListSearchDataSource(
roomListService: RoomListService,
coroutineDispatchers: CoroutineDispatchers,
private val roomSummaryFactory: RoomListRoomSummaryFactory,
) {
private val roomList = roomListService.createRoomList(
pageSize = PAGE_SIZE,
initialFilter = RoomListFilter.None,
source = RoomList.Source.All,
)
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.filteredSummaries
.map { roomSummaries ->
roomSummaries
.map(roomSummaryFactory::create)
.toImmutableList()
}
.flowOn(coroutineDispatchers.computation)
suspend fun setIsActive(isActive: Boolean) = coroutineScope {
if (isActive) {
roomList.loadAllIncrementally(this)
} else {
roomList.reset()
}
}
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
val filter = if (searchQuery.isBlank()) {
RoomListFilter.None
} else {
RoomListFilter.NormalizedMatchRoomName(searchQuery)
}
roomList.updateFilter(filter)
}
}
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.search
sealed interface RoomListSearchEvents {
data object ToggleSearchVisibility : RoomListSearchEvents
data class QueryChanged(val query: String) : RoomListSearchEvents
data object ClearQuery : RoomListSearchEvents
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.search
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 dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import kotlinx.collections.immutable.persistentListOf
@Inject
class RoomListSearchPresenter(
private val dataSource: RoomListSearchDataSource,
) : Presenter<RoomListSearchState> {
@Composable
override fun present(): RoomListSearchState {
// Do not use rememberSaveable so that search is not active when the user navigates back to the screen
var isSearchActive by remember {
mutableStateOf(false)
}
var searchQuery by remember {
mutableStateOf("")
}
LaunchedEffect(isSearchActive) {
dataSource.setIsActive(isSearchActive)
}
LaunchedEffect(searchQuery) {
dataSource.setSearchQuery(searchQuery)
}
fun handleEvent(event: RoomListSearchEvents) {
when (event) {
RoomListSearchEvents.ClearQuery -> {
searchQuery = ""
}
is RoomListSearchEvents.QueryChanged -> {
searchQuery = event.query
}
RoomListSearchEvents.ToggleSearchVisibility -> {
isSearchActive = !isSearchActive
searchQuery = ""
}
}
}
val searchResults by dataSource.roomSummaries.collectAsState(initial = persistentListOf())
return RoomListSearchState(
isSearchActive = isSearchActive,
query = searchQuery,
results = searchResults,
eventSink = ::handleEvent,
)
}
}
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.search
import io.element.android.features.home.impl.model.RoomListRoomSummary
import kotlinx.collections.immutable.ImmutableList
data class RoomListSearchState(
val isSearchActive: Boolean,
val query: String,
val results: ImmutableList<RoomListRoomSummary>,
val eventSink: (RoomListSearchEvents) -> Unit
)
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.search
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
class RoomListSearchStateProvider : PreviewParameterProvider<RoomListSearchState> {
override val values: Sequence<RoomListSearchState>
get() = sequenceOf(
aRoomListSearchState(),
aRoomListSearchState(
isSearchActive = true,
query = "Test",
results = aRoomListRoomSummaryList()
),
)
}
fun aRoomListSearchState(
isSearchActive: Boolean = false,
query: String = "",
results: ImmutableList<RoomListRoomSummary> = persistentListOf(),
eventSink: (RoomListSearchEvents) -> Unit = { },
) = RoomListSearchState(
isSearchActive = isSearchActive,
query = query,
results = results,
eventSink = eventSink,
)
@@ -0,0 +1,202 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.search
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.components.RoomSummaryRow
import io.element.android.features.home.impl.contentType
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FilledTextField
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.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun RoomListSearchView(
state: RoomListSearchState,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit,
onRoomClick: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(enabled = state.isSearchActive) {
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
}
AnimatedVisibility(
visible = state.isSearchActive,
enter = fadeIn(),
exit = fadeOut(),
) {
Column(modifier = modifier) {
RoomListSearchContent(
state = state,
hideInvitesAvatars = hideInvitesAvatars,
onRoomClick = onRoomClick,
eventSink = eventSink,
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomListSearchContent(
state: RoomListSearchState,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit,
onRoomClick: (RoomId) -> Unit,
) {
val borderColor = MaterialTheme.colorScheme.tertiary
val strokeWidth = 1.dp
fun onBackButtonClick() {
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
}
fun onRoomClick(room: RoomListRoomSummary) {
onRoomClick(room.roomId)
}
Scaffold(
topBar = {
TopAppBar(
modifier = Modifier.drawBehind {
drawLine(
color = borderColor,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = strokeWidth.value
)
},
navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
title = {
// TODO replace `state.query` with TextFieldState when it's available for M3 TextField
// The stateSaver will keep the selection state when returning to this UI
var value by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(state.query))
}
val focusRequester = remember { FocusRequester() }
FilledTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = value,
singleLine = true,
onValueChange = {
value = it
state.eventSink(RoomListSearchEvents.QueryChanged(it.text))
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
),
trailingIcon = {
if (value.text.isNotEmpty()) {
IconButton(onClick = {
state.eventSink(RoomListSearchEvents.ClearQuery)
// Clear local state too
value = value.copy(text = "")
}) {
Icon(
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_cancel)
)
}
}
}
)
LaunchedEffect(Unit) {
if (!focusRequester.restoreFocusedChild()) {
focusRequester.requestFocus()
}
focusRequester.saveFocusedChild()
}
},
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
) {
LazyColumn(
modifier = Modifier.weight(1f),
) {
items(
items = state.results,
contentType = { room -> room.contentType() },
) { room ->
RoomSummaryRow(
room = room,
hideInviteAvatars = hideInvitesAvatars,
// TODO
isInviteSeen = false,
onClick = ::onRoomClick,
eventSink = eventSink,
)
}
}
}
}
}
@PreviewsDayNight
@Composable
internal fun RoomListSearchContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview {
RoomListSearchContent(
state = state,
hideInvitesAvatars = false,
onRoomClick = {},
eventSink = {},
)
}
@@ -0,0 +1,11 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
sealed interface HomeSpacesEvents
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.flow.map
@Inject
class HomeSpacesPresenter(
private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore,
) : Presenter<HomeSpacesState> {
@Composable
override fun present(): HomeSpacesState {
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val spaceRooms by remember {
client.spaceService.spaceRoomsFlow.map { it.toImmutableList() }
}.collectAsState(persistentListOf())
val seenSpaceInvites by remember {
seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
}.collectAsState(persistentSetOf())
fun handleEvent(event: HomeSpacesEvents) {
// when (event) { }
}
return HomeSpacesState(
space = CurrentSpace.Root,
spaceRooms = spaceRooms,
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
eventSink = ::handleEvent,
)
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
data class HomeSpacesState(
val space: CurrentSpace,
val spaceRooms: ImmutableList<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit,
)
sealed interface CurrentSpace {
object Root : CurrentSpace
data class Space(val spaceRoom: SpaceRoom) : CurrentSpace
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
override val values: Sequence<HomeSpacesState>
get() = sequenceOf(
aHomeSpacesState(
spaceRooms = SpaceRoomProvider().values.toList(),
seenSpaceInvites = setOf(
RoomId("!spaceId3:example.com"),
),
),
aHomeSpacesState(
space = CurrentSpace.Space(
spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com"))
),
spaceRooms = aListOfSpaceRooms(),
),
)
}
internal fun aHomeSpacesState(
space: CurrentSpace = CurrentSpace.Root,
spaceRooms: List<SpaceRoom> = aListOfSpaceRooms(),
seenSpaceInvites: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false,
eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState(
space = space,
spaceRooms = spaceRooms.toImmutableList(),
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar,
eventSink = eventSink,
)
fun aListOfSpaceRooms(): List<SpaceRoom> {
return listOf(
aSpaceRoom(roomId = RoomId("!spaceId0:example.com")),
aSpaceRoom(roomId = RoomId("!spaceId1:example.com")),
aSpaceRoom(roomId = RoomId("!spaceId2:example.com")),
)
}
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
import io.element.android.libraries.matrix.ui.components.SpaceRoomItemView
import io.element.android.libraries.matrix.ui.model.getAvatarData
import kotlinx.collections.immutable.toImmutableList
@Composable
fun HomeSpacesView(
state: HomeSpacesState,
lazyListState: LazyListState,
onSpaceClick: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
state = lazyListState
) {
val space = state.space
when (space) {
CurrentSpace.Root -> {
item {
SpaceHeaderRootView(numberOfSpaces = state.spaceRooms.size)
}
}
is CurrentSpace.Space -> item {
SpaceHeaderView(
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
name = space.spaceRoom.displayName,
topic = space.spaceRoom.topic,
visibility = space.spaceRoom.visibility,
heroes = space.spaceRoom.heroes.toImmutableList(),
numberOfMembers = space.spaceRoom.numJoinedMembers,
)
}
}
item {
HorizontalDivider()
}
itemsIndexed(
items = state.spaceRooms,
key = { _, spaceRoom -> spaceRoom.roomId }
) { index, spaceRoom ->
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
SpaceRoomItemView(
spaceRoom = spaceRoom,
showUnreadIndicator = isInvitation && spaceRoom.roomId !in state.seenSpaceInvites,
hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = {
onSpaceClick(spaceRoom.roomId)
},
onLongClick = {
// TODO
},
)
if (index != state.spaceRooms.lastIndex) {
HorizontalDivider()
}
}
}
}
@PreviewsDayNight
@Composable
internal fun HomeSpacesViewPreview(
@PreviewParameter(HomeSpacesStateProvider::class) state: HomeSpacesState,
) = ElementPreview {
HomeSpacesView(
state = state,
lazyListState = rememberLazyListState(),
onSpaceClick = {},
modifier = Modifier,
)
}
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom
class SpaceRoomProvider : PreviewParameterProvider<SpaceRoom> {
override val values: Sequence<SpaceRoom> = sequenceOf(
aSpaceRoom(),
aSpaceRoom(
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
roomId = RoomId("!spaceId0:example.com"),
),
aSpaceRoom(
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
avatarUrl = "anUrl",
roomId = RoomId("!spaceId1:example.com"),
),
aSpaceRoom(
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
avatarUrl = "anUrl",
roomId = RoomId("!spaceId2:example.com"),
state = CurrentUserMembership.INVITED,
),
)
}
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_set_up_recovery_content">"Стварыце новы ключ аднаўлення, які можна выкарыстоўваць для аднаўлення зашыфраванай гісторыі паведамленняў у выпадку страты доступу да вашых прылад."</string>
<string name="banner_set_up_recovery_submit">"Наладзьце аднаўленне"</string>
<string name="banner_set_up_recovery_title">"Наладзіць аднаўленне"</string>
<string name="confirm_recovery_key_banner_message">"Пацвердзіце свой ключ аднаўлення, каб захаваць доступ да сховішча ключоў і гісторыі паведамленняў."</string>
<string name="confirm_recovery_key_banner_title">"Ваша сховішча ключоў не сінхранізавана"</string>
<string name="full_screen_intent_banner_message">"Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны."</string>
<string name="full_screen_intent_banner_title">"Палепшыце якасць званкоў"</string>
<string name="screen_home_tab_chats">"Усе чаты"</string>
<string name="screen_invites_decline_chat_message">"Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Адхіліць запрашэнне"</string>
<string name="screen_invites_decline_direct_chat_message">"Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Адхіліць чат"</string>
<string name="screen_invites_empty_list">"Няма запрашэнняў"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) запрасіў(-ла) вас"</string>
<string name="screen_migration_message">"Гэта аднаразовы працэс, дзякуем за чаканне."</string>
<string name="screen_migration_title">"Налада ўліковага запісу."</string>
<string name="screen_roomlist_a11y_create_message">"Стварыце новую размову або пакой"</string>
<string name="screen_roomlist_empty_message">"Пачніце з паведамлення каму-небудзь."</string>
<string name="screen_roomlist_empty_title">"Пакуль няма чатаў."</string>
<string name="screen_roomlist_filter_favourites">"Абранае"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Дадаць чат у абранае можна ў наладах чата.
На дадзены момант вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"У вас пакуль няма абраных чатаў"</string>
<string name="screen_roomlist_filter_invites">"Запрашэнні"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"У вас няма непрынятых запрашэнняў."</string>
<string name="screen_roomlist_filter_low_priority">"Нізкі прыярытэт"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"У вас няма чатаў для гэтай катэгорыі"</string>
<string name="screen_roomlist_filter_people">"Людзі"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"У вас пакуль няма асабістых паведамленняў"</string>
<string name="screen_roomlist_filter_rooms">"Пакоі"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Вас пакуль няма ў ніводным пакоі"</string>
<string name="screen_roomlist_filter_unreads">"Непрачытаныя"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Віншуем!
У вас няма непрачытаных паведамленняў!"</string>
<string name="screen_roomlist_main_space_title">"Усе чаты"</string>
<string name="screen_roomlist_mark_as_read">"Пазначыць як прачытанае"</string>
<string name="screen_roomlist_mark_as_unread">"Пазначыць як непрачытанае"</string>
<string name="session_verification_banner_message">"Здаецца, вы карыстаецеся новай прыладай. Праверце з дапамогай іншай прылады, каб атрымаць доступ да зашыфраваных паведамленняў."</string>
<string name="session_verification_banner_title">"Пацвердзіце, што гэта вы"</string>
</resources>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Потвърдете ключа си за възстановяване, за да запазите достъп до хранилището за ключове и историята на съобщенията си."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Въведете ключа си за възстановяване"</string>
<string name="confirm_recovery_key_banner_title">"Хранилището ви за ключове не е синхронизирано"</string>
<string name="screen_home_tab_chats">"Всички чатове"</string>
<string name="screen_home_tab_spaces">"Пространства"</string>
<string name="screen_invites_decline_chat_message">"Сигурни ли сте, че искате да отхвърлите поканата за присъединяване в %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Отказване на покана"</string>
<string name="screen_invites_decline_direct_chat_message">"Сигурни ли сте, че искате да откажете този личен чат с %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Отказване на чат"</string>
<string name="screen_invites_empty_list">"Няма покани"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) ви покани"</string>
<string name="screen_migration_message">"Това е еднократен процес, благодаря, че изчакахте."</string>
<string name="screen_migration_title">"Настройване на вашия акаунт."</string>
<string name="screen_roomlist_a11y_create_message">"Създаване на нов разговор или стая"</string>
<string name="screen_roomlist_empty_message">"Започнете, като изпратите съобщение на някого."</string>
<string name="screen_roomlist_empty_title">"Все още няма чатове."</string>
<string name="screen_roomlist_filter_favourites">"Любими"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Можете да добавите чат към фаворизираните си в настройките на чата.
Засега можете да премахнете избора на филтрите, за да видите другите си чатове."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Все още нямате фаворизирани чатове"</string>
<string name="screen_roomlist_filter_invites">"Покани"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Нямате чакащи покани."</string>
<string name="screen_roomlist_filter_low_priority">"Нисък приоритет"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Можете да премахнете избора на филтрите, за да видите другите си чатове"</string>
<string name="screen_roomlist_filter_people">"Хора"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Все още нямате директни съобщения"</string>
<string name="screen_roomlist_filter_rooms">"Стаи"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Все още не сте в никоя стая"</string>
<string name="screen_roomlist_filter_unreads">"Непрочетени"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Поздравления!
Нямате непрочетени съобщения!"</string>
<string name="screen_roomlist_main_space_title">"Всички чатове"</string>
<string name="screen_roomlist_mark_as_read">"Отбелязване като прочетено"</string>
<string name="screen_roomlist_mark_as_unread">"Отбелязване като непрочетено"</string>
<string name="session_verification_banner_message">"Изглежда, че използвате ново устройство. Потвърдете с друго устройство за достъп до вашите шифровани съобщения."</string>
<string name="session_verification_banner_title">"Потвърдете, че сте вие"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Zakažte optimalizaci baterie pro tuto aplikaci, abyste měli jistotu, že budou přijata všechna oznámení."</string>
<string name="banner_battery_optimization_submit_android">"Zakázat optimalizaci"</string>
<string name="banner_battery_optimization_title_android">"Nepřicházejí vám oznámení?"</string>
<string name="banner_new_sound_message">"Váš zvuk oznámení byl aktualizován je jasnější, rychlejší a méně rušivý."</string>
<string name="banner_new_sound_title">"Aktualizovali jsme vaše zvuky"</string>
<string name="banner_set_up_recovery_content">"Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením."</string>
<string name="banner_set_up_recovery_submit">"Nastavení obnovy"</string>
<string name="banner_set_up_recovery_title">"Nastavení obnovy"</string>
<string name="confirm_recovery_key_banner_message">"Potvrďte klíč pro obnovení, abyste zachovali přístup k úložišti klíčů a historii zpráv."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Zadejte klíč pro obnovení"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Zapomněli jste klíč pro obnovení?"</string>
<string name="confirm_recovery_key_banner_title">"Vaše úložiště klíčů není synchronizováno"</string>
<string name="full_screen_intent_banner_message">"Abyste nikdy nezmeškali důležitý hovor, změňte nastavení tak, abyste povolili oznámení na celé obrazovce, když je telefon uzamčen."</string>
<string name="full_screen_intent_banner_title">"Vylepšete si zážitek z volání"</string>
<string name="screen_home_tab_chats">"Všechny chaty"</string>
<string name="screen_home_tab_spaces">"Prostory"</string>
<string name="screen_invites_decline_chat_message">"Opravdu chcete odmítnout pozvánku do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odmítnout pozvání"</string>
<string name="screen_invites_decline_direct_chat_message">"Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Odmítnout chat"</string>
<string name="screen_invites_empty_list">"Žádné pozvánky"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) vás pozval(a)"</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_clear_filters">"Vymazat filtry"</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>
<string name="screen_roomlist_filter_favourites">"Oblíbené"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"V nastavení chatu můžete přidat chat k oblíbeným.
Prozatím můžete zrušit výběr filtrů, abyste viděli své další chaty"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Zatím nemáte oblíbené chaty"</string>
<string name="screen_roomlist_filter_invites">"Pozvánky"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Nemáte žádné nevyřízené pozvánky."</string>
<string name="screen_roomlist_filter_low_priority">"Nízká priorita"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Zatím nemáte žádné chaty s nízkou prioritou"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Můžete zrušit výběr filtrů, abyste viděli své další chaty"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Nemáte chaty pro tento výběr"</string>
<string name="screen_roomlist_filter_people">"Lidé"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Zatím nemáte žádné přímé zprávy"</string>
<string name="screen_roomlist_filter_rooms">"Místnosti"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Ještě nejste v žádné místnosti"</string>
<string name="screen_roomlist_filter_unreads">"Nepřečtené"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Gratulujeme!
Nemáte žádné nepřečtené zprávy!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Žádost o vstup odeslána"</string>
<string name="screen_roomlist_main_space_title">"Všechny chaty"</string>
<string name="screen_roomlist_mark_as_read">"Označit jako přečtené"</string>
<string name="screen_roomlist_mark_as_unread">"Označit jako nepřečtené"</string>
<string name="screen_roomlist_tombstoned_room_description">"Tato místnost byla aktualizována"</string>
<string name="session_verification_banner_message">"Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám."</string>
<string name="session_verification_banner_title">"Ověřte, že jste to vy"</string>
</resources>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Analluogwch optimeiddio batri ar gyfer yr ap hwn, er mwyn sicrhau bod pob hysbysiad yn cael ei dderbyn."</string>
<string name="banner_battery_optimization_submit_android">"Analluogi optimeiddio"</string>
<string name="banner_battery_optimization_title_android">"Hysbysiadau ddim yn cyrraedd?"</string>
<string name="banner_set_up_recovery_content">"Adferwch eich hunaniaeth cryptograffig a hanes negeseuon gydag allwedd adfer os ydych wedi colli eich holl ddyfeisiau presennol."</string>
<string name="banner_set_up_recovery_submit">"Gosod adfer"</string>
<string name="banner_set_up_recovery_title">"Gosodwch adferiad i ddiogelu eich cyfrif"</string>
<string name="confirm_recovery_key_banner_message">"Cadarnhewch eich allwedd adfer i gynnal mynediad i\'ch storfa allweddi a\'ch hanes negeseuon."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Rhowch eich allwedd adfer"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Wedi anghofio\'ch allwedd adfer?"</string>
<string name="confirm_recovery_key_banner_title">"Dyw eich allwedd storfa heb ei gydweddu"</string>
<string name="full_screen_intent_banner_message">"Er mwyn sicrhau fyddwch chi ddim yn colli galwad bwysig, newidiwch eich gosodiadau i ganiatáu hysbysiadau sgrin lawn pan fydd eich ffôn wedi\'i gloi."</string>
<string name="full_screen_intent_banner_title">"Gwella profiad eich galwadau"</string>
<string name="screen_home_tab_chats">"Sgyrsiau"</string>
<string name="screen_home_tab_spaces">"Gofodau"</string>
<string name="screen_invites_decline_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Gwrthod y gwahoddiad"</string>
<string name="screen_invites_decline_direct_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Gwrthod sgwrs"</string>
<string name="screen_invites_empty_list">"Dim Gwahoddiadau"</string>
<string name="screen_invites_invited_you">"Mae %1$s (%2$s) wedi eich gwahodd"</string>
<string name="screen_migration_message">"Mae hon yn broses un tro, diolch am aros."</string>
<string name="screen_migration_title">"Creu eich cyfrif."</string>
<string name="screen_roomlist_a11y_create_message">"Crëwch sgwrs neu ystafell newydd"</string>
<string name="screen_roomlist_clear_filters">"Clirio\'r hidlau"</string>
<string name="screen_roomlist_empty_message">"Cychwynnwch arni trwy anfon neges at rywun."</string>
<string name="screen_roomlist_empty_title">"Dim sgyrsiau eto."</string>
<string name="screen_roomlist_filter_favourites">"Ffefrynnau"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Gallwch ychwanegu sgwrs at eich ffefrynnau yn y gosodiadau sgwrsio.
Am y tro, gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Does gennych chi ddim hoff sgyrsiau eto"</string>
<string name="screen_roomlist_filter_invites">"Gwahoddiadau"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Does gennych chi ddim gwahoddiadau yn aros."</string>
<string name="screen_roomlist_filter_low_priority">"Blaenoriaeth Isel"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Does gennych chi ddim sgyrsiau blaenoriaeth isel eto"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Does gennych chi ddim sgyrsiau ar gyfer y dewis hwn"</string>
<string name="screen_roomlist_filter_people">"Pobl"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Does gennych chi ddim unrhyw DMs eto"</string>
<string name="screen_roomlist_filter_rooms">"Ystafelloedd"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Dydych chi ddim mewn unrhyw ystafell eto"</string>
<string name="screen_roomlist_filter_unreads">"Heb ei ddarllen"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Llongyfarchiadau!
Does gennych chi ddim negeseuon heb eu darllen!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Anfonwyd y cais i ymuno"</string>
<string name="screen_roomlist_main_space_title">"Sgyrsiau"</string>
<string name="screen_roomlist_mark_as_read">"Marcio fel wedi\'i ddarllen"</string>
<string name="screen_roomlist_mark_as_unread">"Marcio fel heb ei ddarllen"</string>
<string name="screen_roomlist_tombstoned_room_description">"Mae\'r ystafell hon wedi\'i huwchraddio"</string>
<string name="session_verification_banner_message">"Mae\'n debyg eich bod chi\'n defnyddio dyfais newydd. Dilyswch gyda dyfais arall i gael mynediad at eich negeseuon wedi\'u hamgryptio."</string>
<string name="session_verification_banner_title">"Gwiriwch mai chi sydd yna"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Deaktiver batterioptimering for denne app for at sikre, at alle notifikationer dukker op."</string>
<string name="banner_battery_optimization_submit_android">"Deaktivér optimering"</string>
<string name="banner_battery_optimization_title_android">"Modtager du ikke notifikationer?"</string>
<string name="banner_new_sound_message">"Dit notifikationsping er blevet opdateret tydeligere, hurtigere og mindre forstyrrende."</string>
<string name="banner_new_sound_title">"Vi har opdateret dine lyde"</string>
<string name="banner_set_up_recovery_content">"Gendan din kryptografiske identitet og meddelelseshistorik med en gendannelsesnøgle, hvis du har mistet alle dine eksisterende enheder."</string>
<string name="banner_set_up_recovery_submit">"Opsæt gendannelse"</string>
<string name="banner_set_up_recovery_title">"Konfigurer gendannelse for at beskytte din konto"</string>
<string name="confirm_recovery_key_banner_message">"Bekræft din gendannelsesnøgle for at bevare adgangen til nøglelager og meddelelseshistorik."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Indtast din gendannelsesnøgle"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Har du glemt din gendannelsesnøgle?"</string>
<string name="confirm_recovery_key_banner_title">"Dit nøglelager er ikke synkroniseret"</string>
<string name="full_screen_intent_banner_message">"For at sikre, at du aldrig går glip af et vigtigt opkald, skal du ændre dine indstillinger til at tillade underretninger i fuld skærm, når din telefon er låst."</string>
<string name="full_screen_intent_banner_title">"Gør din opkaldsoplevelse bedre"</string>
<string name="screen_home_tab_chats">"Samtaler"</string>
<string name="screen_home_tab_spaces">"Grupper"</string>
<string name="screen_invites_decline_chat_message">"Er du sikker på, at du vil afvise invitationen til at deltage i %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Afvis invitation"</string>
<string name="screen_invites_decline_direct_chat_message">"Er du sikker på, at du vil afvise denne private samtale med %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Afvis samtale"</string>
<string name="screen_invites_empty_list">"Ingen invitationer"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s ) inviterede dig"</string>
<string name="screen_migration_message">"Dette er en engangsproces, tak for din tålmodighed."</string>
<string name="screen_migration_title">"Sætter din konto op."</string>
<string name="screen_roomlist_a11y_create_message">"Opret en ny samtale eller et nyt rum"</string>
<string name="screen_roomlist_clear_filters">"Ryd filtre"</string>
<string name="screen_roomlist_empty_message">"Kom i gang ved at sende en besked til nogen."</string>
<string name="screen_roomlist_empty_title">"Ingen samtaler endnu."</string>
<string name="screen_roomlist_filter_favourites">"Favoritter"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Du kan tilføje en samtale til dine favoritter i samtaleindstillingerne.
For nu kan du fravælge filtre for at se dine andre samtaler"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Du har endnu ingen foretrukne samtaler"</string>
<string name="screen_roomlist_filter_invites">"Invitationer"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Du har ingen afventende invitationer."</string>
<string name="screen_roomlist_filter_low_priority">"Lav prioritet"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Du har endnu ingen chats med lav prioritet"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Du kan fravælge filtre for at se dine andre samtaler"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Du har ingen samtaler til dette valg"</string>
<string name="screen_roomlist_filter_people">"Brugere"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Du har ingen DM\'er endnu"</string>
<string name="screen_roomlist_filter_rooms">"Rum"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Du er ikke i noget rum endnu"</string>
<string name="screen_roomlist_filter_unreads">"Ulæste"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Tillykke!
Du har ingen ulæste beskeder!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Anmodning om at deltage sendt"</string>
<string name="screen_roomlist_main_space_title">"Samtaler"</string>
<string name="screen_roomlist_mark_as_read">"Marker som læst"</string>
<string name="screen_roomlist_mark_as_unread">"Marker som ulæst"</string>
<string name="screen_roomlist_tombstoned_room_description">"Dette rum er blevet opgraderet"</string>
<string name="session_verification_banner_message">"Det ser ud til, at du bruger en ny enhed. Bekræft med en anden enhed for at få adgang til dine krypterede meddelelser."</string>
<string name="session_verification_banner_title">"Bekræft, at det er dig"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Deaktiviere die Batterieoptimierung für diese App, um sicherzustellen, dass alle Benachrichtigungen empfangen werden."</string>
<string name="banner_battery_optimization_submit_android">"Optimierung deaktivieren"</string>
<string name="banner_battery_optimization_title_android">"Kommen die Benachrichtigungen nicht an?"</string>
<string name="banner_new_sound_message">"Dein Benachrichtigungs-Ping wurde aktualisiert klarer, schneller und weniger störend."</string>
<string name="banner_new_sound_title">"Wir haben deine Sounds aktualisiert"</string>
<string name="banner_set_up_recovery_content">"Stelle Deine kryptographische Identität und Deinen Nachrichtenverlauf mit Hilfe eines Wiederherstellungsschlüssels wieder her, falls du alle deine Geräte verloren haben solltest"</string>
<string name="banner_set_up_recovery_submit">"Wiederherstellung einrichten"</string>
<string name="banner_set_up_recovery_title">"Wiederherstellung einrichten"</string>
<string name="confirm_recovery_key_banner_message">"Bestätige deinen Wiederherstellungsschlüssel, um weiterhin auf deinen Schlüsselspeicher und den Nachrichtenverlauf zugreifen zu können."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Gib deinen Wiederherstellungsschlüssel ein"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Hast du deinen Wiederherstellungsschlüssel vergessen?"</string>
<string name="confirm_recovery_key_banner_title">"Dein Schlüsselspeicher ist nicht synchronisiert"</string>
<string name="full_screen_intent_banner_message">"Damit du keinen wichtigen Anruf verpasst, ändere bitte deine Einstellungen so, dass du bei gesperrtem Telefon Benachrichtigungen im Vollbildmodus erhältst."</string>
<string name="full_screen_intent_banner_title">"Verbessere dein Anruferlebnis"</string>
<string name="screen_home_tab_chats">"Chats"</string>
<string name="screen_home_tab_spaces">"Spaces"</string>
<string name="screen_invites_decline_chat_message">"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
<string name="screen_invites_decline_direct_chat_message">"Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?"</string>
<string name="screen_invites_decline_direct_chat_title">"Einladung ablehnen"</string>
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</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">"Einen neuen Chat erstellen"</string>
<string name="screen_roomlist_clear_filters">"Filter zurücksetzen"</string>
<string name="screen_roomlist_empty_message">"Schick einfach jemandem eine Nachricht, um loszulegen."</string>
<string name="screen_roomlist_empty_title">"Noch keine Chats."</string>
<string name="screen_roomlist_filter_favourites">"Favoriten"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"In den Chat Einstellungen kannst du einen Chat als Favorit markieren.
Deaktiviere den entsprechenden Filter, um deine anderen Chats zu sehen"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Du hast noch keine Chats als Favorit markiert"</string>
<string name="screen_roomlist_filter_invites">"Einladungen"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Du hast keine ausstehenden Einladungen."</string>
<string name="screen_roomlist_filter_low_priority">"Niedrige Priorität"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Du hast noch keine Chats mit niedriger Priorität."</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Wähle Filter ab, um Chats zu sehen."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Für diese Auswahl hast du keinen Chat."</string>
<string name="screen_roomlist_filter_people">"Personen"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Du hast noch keine Direktnachrichten"</string>
<string name="screen_roomlist_filter_rooms">"Gruppen"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Du bist noch in keinem Chat"</string>
<string name="screen_roomlist_filter_unreads">"Ungelesen"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Glückwunsch!
Du hast keine ungelesenen Nachrichten!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Beitrittsanfrage geschickt"</string>
<string name="screen_roomlist_main_space_title">"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="screen_roomlist_tombstoned_room_description">"Die Chat-Version wurde aktualisiert"</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">"Verifiziere deine Identität"</string>
</resources>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Απενεργοποίησε τη βελτιστοποίηση μπαταρίας για αυτήν την εφαρμογή, για να βεβαιωθείς ότι λαμβάνονται όλες οι ειδοποιήσεις."</string>
<string name="banner_battery_optimization_submit_android">"Απενεργοποίηση βελτιστοποίησης"</string>
<string name="banner_battery_optimization_title_android">"Δεν φτάνουν οι ειδοποιήσεις;"</string>
<string name="banner_set_up_recovery_content">"Δημιούργησε ένα νέο κλειδί ανάκτησης που μπορεί να χρησιμοποιηθεί για την επαναφορά του ιστορικού των κρυπτογραφημένων μηνυμάτων σου σε περίπτωση που χάσεις την πρόσβαση στις συσκευές σου."</string>
<string name="banner_set_up_recovery_submit">"Ρύθμιση ανάκτησης"</string>
<string name="banner_set_up_recovery_title">"Ρύθμιση ανάκτησης"</string>
<string name="confirm_recovery_key_banner_message">"Επιβεβαίωσε το κλειδί ανάκτησης για να διατηρήσεις την πρόσβαση στο χώρο αποθήκευσης κλειδιών και στο ιστορικό μηνυμάτων."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Εισήγαγε το κλειδί ανάκτησης"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Ξέχασες το κλειδί ανάκτησης;"</string>
<string name="confirm_recovery_key_banner_title">"Ο χώρος αποθήκευσης κλειδιών σου δεν είναι συγχρονισμένος"</string>
<string name="full_screen_intent_banner_message">"Για να διασφαλίσετε ότι δεν θα χάσετε ποτέ μια σημαντική κλήση, αλλάξτε τις ρυθμίσεις σας ώστε να επιτρέπονται οι ειδοποιήσεις πλήρους οθόνης όταν το τηλέφωνό σας είναι κλειδωμένο."</string>
<string name="full_screen_intent_banner_title">"Βελτίωσε την εμπειρία κλήσεων"</string>
<string name="screen_home_tab_chats">"Συνομιλίες"</string>
<string name="screen_invites_decline_chat_message">"Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;"</string>
<string name="screen_invites_decline_chat_title">"Απόρριψη πρόσκλησης"</string>
<string name="screen_invites_decline_direct_chat_message">"Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;"</string>
<string name="screen_invites_decline_direct_chat_title">"Απόρριψη συνομιλίας"</string>
<string name="screen_invites_empty_list">"Χωρίς προσκλήσεις"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) σέ προσκάλεσε"</string>
<string name="screen_migration_message">"Αυτή είναι μια εφάπαξ διαδικασία, ευχαριστώ που περίμενες."</string>
<string name="screen_migration_title">"Ρύθμιση του λογαριασμού σου."</string>
<string name="screen_roomlist_a11y_create_message">"Δημιουργία νέας συνομιλίας ή αίθουσας"</string>
<string name="screen_roomlist_empty_message">"Ξεκίνησε στέλνοντας μηνύματα σε κάποιον."</string>
<string name="screen_roomlist_empty_title">"Δεν υπάρχουν συνομιλίες ακόμα."</string>
<string name="screen_roomlist_filter_favourites">"Αγαπημένα"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Μπορείς να προσθέσεις μια συνομιλία στα αγαπημένα σου στις ρυθμίσεις συνομιλίας.
Προς το παρόν, μπορείς να καταργήσεις την επιλογή φίλτρων για να δεις τις άλλες συνομιλίες σου"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Δεν έχεις ακόμα αγαπημένες συνομιλίες"</string>
<string name="screen_roomlist_filter_invites">"Προσκλήσεις"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Δεν έχεις εκκρεμείς προσκλήσεις."</string>
<string name="screen_roomlist_filter_low_priority">"Χαμηλής Προτεραιότητας"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Μπορείς να καταργήσεις την επιλογή φίλτρων για να δεις τις άλλες συνομιλίες σου"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Δεν έχεις συνομιλίες για αυτήν την επιλογή"</string>
<string name="screen_roomlist_filter_people">"Άτομα"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Δεν έχεις ακόμα ΠΜ"</string>
<string name="screen_roomlist_filter_rooms">"Αίθουσες"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Δεν είστε ακόμα σε κάποια αίθουσα"</string>
<string name="screen_roomlist_filter_unreads">"Μη αναγνωσμένα"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Συγχαρητήρια!
Δεν έχεις μη αναγνωσμένα μηνύματα!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Το αίτημα συμμετοχής στάλθηκε"</string>
<string name="screen_roomlist_main_space_title">"Συνομιλίες"</string>
<string name="screen_roomlist_mark_as_read">"Επισήμανση ως αναγνωσμένου"</string>
<string name="screen_roomlist_mark_as_unread">"Επισήμανση ως μη αναγνωσμένου"</string>
<string name="screen_roomlist_tombstoned_room_description">"Αυτή η αίθουσα έχει αναβαθμιστεί"</string>
<string name="session_verification_banner_message">"Φαίνεται ότι χρησιμοποιείς μια νέα συσκευή. Επαλήθευσε με άλλη συσκευή για πρόσβαση στα κρυπτογραφημένα σου μηνύματα."</string>
<string name="session_verification_banner_title">"Επαλήθευσε ότι είσαι εσύ"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Disable battery optimization for this app, to make sure all notifications are received."</string>
<string name="banner_battery_optimization_submit_android">"Disable optimization"</string>
<string name="screen_roomlist_filter_favourites">"Favorites"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"You can add a chat to your favorites in the chat settings.
For now, you can deselect filters in order to see your other chats"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"You dont have favorite chats yet"</string>
</resources>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_set_up_recovery_content">"Recupera tu identidad criptográfica y tu historial de mensajes con una clave de recuperación si has perdido todos tus dispositivos actuales."</string>
<string name="banner_set_up_recovery_submit">"Configurar la recuperación"</string>
<string name="banner_set_up_recovery_title">"Configura la recuperación para proteger tu cuenta"</string>
<string name="confirm_recovery_key_banner_message">"Confirma tu clave de recuperación para mantener el acceso a tu almacén de claves y al historial de mensajes."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Introduce tu clave de recuperación"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"¿Olvidaste tu clave de recuperación?"</string>
<string name="confirm_recovery_key_banner_title">"Tu almacén de claves no está sincronizado"</string>
<string name="full_screen_intent_banner_message">"Para asegurarte de que nunca te pierdas una llamada importante, modifica tus ajustes para permitir notificaciones a pantalla completa cuando el teléfono esté bloqueado."</string>
<string name="full_screen_intent_banner_title">"Mejora tu experiencia de llamada"</string>
<string name="screen_home_tab_chats">"Chats"</string>
<string name="screen_invites_decline_chat_message">"¿Estás seguro de que quieres rechazar la invitación a unirte a %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Rechazar la invitación"</string>
<string name="screen_invites_decline_direct_chat_message">"¿Estás seguro de que quieres rechazar este chat privado con %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Rechazar el chat"</string>
<string name="screen_invites_empty_list">"Sin invitaciones"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) te invitó"</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>
<string name="screen_roomlist_filter_favourites">"Favoritos"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Puedes añadir un chat a tus favoritos en la configuración del chat.
Por ahora, puedes deseleccionar los filtros para ver tus otros chats"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Aún no tienes chats favoritos"</string>
<string name="screen_roomlist_filter_invites">"Invitaciones"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"No tienes ninguna invitación pendiente."</string>
<string name="screen_roomlist_filter_low_priority">"Prioridad baja"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Puedes deseleccionar filtros para ver tus otros chats."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"No tienes chats para esta selección"</string>
<string name="screen_roomlist_filter_people">"Personas"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Todavía no tienes ningún mensaje directo"</string>
<string name="screen_roomlist_filter_rooms">"Salas"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Todavía no estás en ninguna sala"</string>
<string name="screen_roomlist_filter_unreads">"No leídos"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"¡Felicidades!
¡No tienes ningún mensaje sin leer!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Solicitud de unión enviada"</string>
<string name="screen_roomlist_main_space_title">"Chats"</string>
<string name="screen_roomlist_mark_as_read">"Marcar como leído"</string>
<string name="screen_roomlist_mark_as_unread">"Marcar como no leído"</string>
<string name="session_verification_banner_message">"Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados."</string>
<string name="session_verification_banner_title">"Verifica que eres tú"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Kui tahad olla kindel, et näed õigel ajal kõiki teavitusi, siis palun lülita akukasutuse optimeerimine välja."</string>
<string name="banner_battery_optimization_submit_android">"Lülita akukasutuse optimeerimine välja"</string>
<string name="banner_battery_optimization_title_android">"Sa ei näe kõiki teavitusi?"</string>
<string name="banner_new_sound_message">"Sinu nutiseadme teavituste heli on uuenenud - see on nüüd selgem, kiirem ja vähem häiriv."</string>
<string name="banner_new_sound_title">"Oleme sinu helisid värskendanud"</string>
<string name="banner_set_up_recovery_content">"Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele."</string>
<string name="banner_set_up_recovery_submit">"Seadista andmete taastamine"</string>
<string name="banner_set_up_recovery_title">"Seadista taastamine"</string>
<string name="confirm_recovery_key_banner_message">"Säilitamaks ligipääsu vestluste ja krüptovõtmete varukoopiale, palun sisesta kinnituseks oma taastevõti."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Sisesta oma taastevõti"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Kas unustasid oma taastevõtme?"</string>
<string name="confirm_recovery_key_banner_title">"Sinu võtmehoidla pole sünkroonis"</string>
<string name="full_screen_intent_banner_message">"Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused."</string>
<string name="full_screen_intent_banner_title">"Sinu tõhusad telefonikõned"</string>
<string name="screen_home_tab_chats">"Vestlused"</string>
<string name="screen_home_tab_spaces">"Kogukonnad"</string>
<string name="screen_invites_decline_chat_message">"Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Lükka kutse tagasi"</string>
<string name="screen_invites_decline_direct_chat_message">"Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Keeldu vestlusest"</string>
<string name="screen_invites_empty_list">"Kutseid pole"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) saatis sulle kutse"</string>
<string name="screen_migration_message">"Tänud, et ootad - seda toimingut on vaja teha vaid üks kord."</string>
<string name="screen_migration_title">"Seadistame sinu kasutajakontot."</string>
<string name="screen_roomlist_a11y_create_message">"Loo uus vestlus või jututuba"</string>
<string name="screen_roomlist_clear_filters">"Tühjenda filtrid"</string>
<string name="screen_roomlist_empty_message">"Alustamiseks saada kellelegi sõnum."</string>
<string name="screen_roomlist_empty_title">"Veel pole vestlusi."</string>
<string name="screen_roomlist_filter_favourites">"Lemmikud"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Vestluse seadistusest saad ta määrata lemmikuks.
Aga seni… oma teiste vestluste nägemiseks pead eemaldama filtrid"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Sul veel pole lemmikvestlusi"</string>
<string name="screen_roomlist_filter_invites">"Kutsed"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Sul pole ootel kutseid."</string>
<string name="screen_roomlist_filter_low_priority">"Vähetähtis"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Sul pole veel ühtegi olulist vestlust"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Oma teiste vestluste nägemiseks sa pead filtrid eemaldama"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Selle valiku jaoks sul veel pole vestlusi"</string>
<string name="screen_roomlist_filter_people">"Inimesed"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Sul pole veel otsevestlusi"</string>
<string name="screen_roomlist_filter_rooms">"Jututoad"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Sa veel ei osale mitte üheski jututoas"</string>
<string name="screen_roomlist_filter_unreads">"Lugemata"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Õnnitleme!
Sul pole ühtegi lugemata sõnumit!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Liitumispalve on saadetud"</string>
<string name="screen_roomlist_main_space_title">"Vestlused"</string>
<string name="screen_roomlist_mark_as_read">"Märgi loetuks"</string>
<string name="screen_roomlist_mark_as_unread">"Märgi mitteloetuks"</string>
<string name="screen_roomlist_tombstoned_room_description">"See jututuba on uuendatud"</string>
<string name="session_verification_banner_message">"Tundub, et kasutad uut seadet. Oma krüptitud sõnumite lugemiseks verifitseeri ta mõne muu oma seadmega."</string>
<string name="session_verification_banner_title">"Verifitseeri, et see oled sina"</string>
</resources>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Desgaitu bateriaren optimizazioa aplikazio honentzat, ziurtatzeko jakinarazpen guztiak jasoko direla."</string>
<string name="banner_battery_optimization_submit_android">"Desgaitu optimizazioa"</string>
<string name="banner_battery_optimization_title_android">"Jakinarazpenak ez dira iristen?"</string>
<string name="banner_set_up_recovery_submit">"Konfiguratu berreskurapena"</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Sartu zure berreskuratze-gakoa"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Berreskuratze-gakoa ahaztu al duzu?"</string>
<string name="full_screen_intent_banner_message">"Dei garrantzitsurik galduko ez duzula ziurtatzeko, aldatu ezarpenak telefonoa blokeatuta dagoenean pantaila osoko jakinarazpenak baimentzeko."</string>
<string name="full_screen_intent_banner_title">"Hobetu deien esperientzia"</string>
<string name="screen_home_tab_chats">"Txatak"</string>
<string name="screen_invites_decline_chat_message">"Ziur %1$s(e)ra batzeko gonbidapena baztertu nahi duzula?"</string>
<string name="screen_invites_decline_chat_title">"Baztertu gonbidapena"</string>
<string name="screen_invites_decline_direct_chat_message">"Ziur %1$s(r)en txat pribatua baztertu nahi duzula?"</string>
<string name="screen_invites_decline_direct_chat_title">"Baztertu txata"</string>
<string name="screen_invites_empty_list">"Ez dago gonbidapenik"</string>
<string name="screen_invites_invited_you">"%1$s(e)k (%2$s) gonbidatu zaitu"</string>
<string name="screen_migration_message">"Behin egin beharreko prozesua da; eskerrik asko itxaroteagatik."</string>
<string name="screen_migration_title">"Zure kontua konfiguratzen."</string>
<string name="screen_roomlist_a11y_create_message">"Sortu elkarrizketa edo gela berria"</string>
<string name="screen_roomlist_clear_filters">"Garbitu iragazkiak"</string>
<string name="screen_roomlist_empty_message">"Hasi norbaiti mezuak bidaltzen."</string>
<string name="screen_roomlist_empty_title">"Oraindik ez dago txatik."</string>
<string name="screen_roomlist_filter_favourites">"Gogokoak"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Txatak gogokoetara gehi dezakezu txaten ezarpenetan.
Oraingoz, iragazkiak desautatu ditzakezu zure gainerako txatak ikusteko"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Oraindik ez duzu gogoko txatik"</string>
<string name="screen_roomlist_filter_invites">"Gonbidapenak"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Ez duzu gonbidapenik zain."</string>
<string name="screen_roomlist_filter_low_priority">"Lehentasun baxua"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Iragazkiak desautatu ditzakezu gainerako txatak ikusteko"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Ez duzu hautaketa betetzen duen txatik"</string>
<string name="screen_roomlist_filter_people">"Jendea"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Oraindik ez duzu Mezu Pribaturik"</string>
<string name="screen_roomlist_filter_rooms">"Gelak"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Oraindik ez zaude inolako gelatan"</string>
<string name="screen_roomlist_filter_unreads">"Irakurri gabeak"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Bejondeizula!
Ez duzu irakurri gabeko mezurik!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Sartzeko eskaera bidali da"</string>
<string name="screen_roomlist_main_space_title">"Txatak"</string>
<string name="screen_roomlist_mark_as_read">"Markatu irakurritzat"</string>
<string name="screen_roomlist_mark_as_unread">"Markatu irakurri gabetzat"</string>
<string name="session_verification_banner_message">"Gailu berri bat erabiltzen ari zarela dirudi. Egiaztatu beste gailu batekin enkriptatutako mezuak atzitzeko."</string>
<string name="session_verification_banner_title">"Egiaztatu zu zarela"</string>
</resources>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"از کار انداختن بهینه‌سازی باتری برای این کاره برای اطمینان از گرفتن همهٔ آگاهی‌ها."</string>
<string name="banner_battery_optimization_submit_android">"از کار انداختن بهینه سازی"</string>
<string name="banner_battery_optimization_title_android">"آگاهی‌ها نمی‌رسند؟"</string>
<string name="banner_set_up_recovery_content">"بازگردانی تاریخچهٔ پیام‌ها و هویت رمزنگاشته‌تان با کلید بازیابی در صورت از دست دادن همهٔ افزاره‌های موجودتان."</string>
<string name="banner_set_up_recovery_submit">"برپایی بازیابی"</string>
<string name="banner_set_up_recovery_title">"برپایی بازیابی"</string>
<string name="confirm_recovery_key_banner_message">"کلید بازیابی خود را تأیید کنید تا دسترسی به حافظه کلیدها و تاریخچه پیام‌هایتان حفظ شود ."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"ورود کلید بازیابیتان"</string>
<string name="confirm_recovery_key_banner_title">"ذخیره‌ساز کلیدتان از هم‌گام بودن در آمده"</string>
<string name="full_screen_intent_banner_title">"بهبود تجریهٔ تماستان"</string>
<string name="screen_home_tab_chats">"گپ‌ها"</string>
<string name="screen_home_tab_spaces">"فضاها"</string>
<string name="screen_invites_decline_chat_message">"مطمئنید که می‌خواهید دعوت پیوستن به %1$s را رد کنید؟"</string>
<string name="screen_invites_decline_chat_title">"رد دعوت"</string>
<string name="screen_invites_decline_direct_chat_message">"مطمئنید که می‌خواهید این گپ خصوصی با %1$s را رد کنید؟"</string>
<string name="screen_invites_decline_direct_chat_title">"رد گپ"</string>
<string name="screen_invites_empty_list">"بدون دعوت"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) دعوتتان کرد"</string>
<string name="screen_migration_message">"فرایندی یک باره است. ممنون از شکیباییتان."</string>
<string name="screen_migration_title">"برپایی حسابتان."</string>
<string name="screen_roomlist_a11y_create_message">"ایجاد اتاق یا گفت‌وگویی جدید"</string>
<string name="screen_roomlist_clear_filters">"پاک کردن پالایه‌ها"</string>
<string name="screen_roomlist_empty_message">"آغاز با پیام دادن به کسی."</string>
<string name="screen_roomlist_empty_title">"هنوز گپی وجود ندارد."</string>
<string name="screen_roomlist_filter_favourites">"علاقه‌مندی‌ها"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"هنوز هیچ گپ مورد علاقه‌ای ندارید"</string>
<string name="screen_roomlist_filter_invites">"دعوت‌ها"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"هیچ دعوت منتظری ندارید."</string>
<string name="screen_roomlist_filter_low_priority">"اولویت کم"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"می توانید پالایه‌ها را برای دیدن دیگر گپ‌هایتان بردارید"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"هیچ گپی برای این گزینش ندارید"</string>
<string name="screen_roomlist_filter_people">"افراد"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"هنوز هیچ پیام مستقیمی ندارید"</string>
<string name="screen_roomlist_filter_rooms">"اتاق‌ها"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"هنوز در هیچ اتاقی نیستید"</string>
<string name="screen_roomlist_filter_unreads">"نخوانده‌ها"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"تبریک!
هیچ پیام نخوانده‌ای ندارید!"</string>
<string name="screen_roomlist_knock_event_sent_description">"درخواست پیوستن فرستاده شد"</string>
<string name="screen_roomlist_main_space_title">"گپ‌ها"</string>
<string name="screen_roomlist_mark_as_read">"علامت‌گذاری به عنوان خوانده شده"</string>
<string name="screen_roomlist_mark_as_unread">"نشان به ناخوانده"</string>
<string name="screen_roomlist_tombstoned_room_description">"این اتاق ارتقا یافته"</string>
<string name="session_verification_banner_message">"گویا از افزاره‌ای جدید استفاده می‌کنید. تأیید با افزاره‌ای دیگر برای دسترسی به پیام‌های رمزنگاری شده‌تان."</string>
<string name="session_verification_banner_title">"تأیید کنید که خودتانید"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Ota tämän sovelluksen akunkäytön optimointi pois käytöstä varmistaaksesi, että kaikki ilmoitukset tulevat perille."</string>
<string name="banner_battery_optimization_submit_android">"Ota optimointi pois käytöstä"</string>
<string name="banner_battery_optimization_title_android">"Eikö ilmoitukset tule perille?"</string>
<string name="banner_new_sound_message">"Ilmoitusääni on päivitetty — selkeämpi, nopeampi ja vähemmän häiritsevä."</string>
<string name="banner_new_sound_title">"Olemme päivittäneet äänesi"</string>
<string name="banner_set_up_recovery_content">"Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, mikäli menetät pääsyn kaikkiin laitteisiisi."</string>
<string name="banner_set_up_recovery_submit">"Ota palautus käyttöön"</string>
<string name="banner_set_up_recovery_title">"Ota palautus käyttöön tilisi suojaamiseksi"</string>
<string name="confirm_recovery_key_banner_message">"Vahvista palautusavaimesi, jotta pääset edelleen käyttämään avainten säilytystä ja viestihistoriaa."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Syötä palautusavaimesi"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Unohditko palautusavaimesi?"</string>
<string name="confirm_recovery_key_banner_title">"Avainten säilytys ei ole synkronoitu"</string>
<string name="full_screen_intent_banner_message">"Salli koko näytön ilmoitukset, kun laite on lukittu, jos et halua koskaan missata tärkeää puhelua."</string>
<string name="full_screen_intent_banner_title">"Paranna puhelukokemustasi"</string>
<string name="screen_home_tab_chats">"Keskustelut"</string>
<string name="screen_home_tab_spaces">"Tilat"</string>
<string name="screen_invites_decline_chat_message">"Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?"</string>
<string name="screen_invites_decline_chat_title">"Hylkää kutsu"</string>
<string name="screen_invites_decline_direct_chat_message">"Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?"</string>
<string name="screen_invites_decline_direct_chat_title">"Hylkää keskustelu"</string>
<string name="screen_invites_empty_list">"Ei kutsuja"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) kutsui sinut"</string>
<string name="screen_migration_message">"Tämä on kertaluonteinen prosessi, kiitos odottamisesta."</string>
<string name="screen_migration_title">"Tiliä määritetään."</string>
<string name="screen_roomlist_a11y_create_message">"Luo uusi keskustelu tai huone"</string>
<string name="screen_roomlist_clear_filters">"Tyhjennä suodattimet"</string>
<string name="screen_roomlist_empty_message">"Aloita lähettämällä viesti jollekin."</string>
<string name="screen_roomlist_empty_title">"Sinulla ei ole vielä keskusteluja."</string>
<string name="screen_roomlist_filter_favourites">"Suosikit"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Voit lisätä keskustelun suosikkeihisi keskustelun asetuksissa.
Toistaiseksi voit poistaa suodattimien valinnan, jotta näet muut keskustelut."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Sinulla ei ole vielä suosikkikeskusteluja"</string>
<string name="screen_roomlist_filter_invites">"Kutsut"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Sinulla ei ole yhtään odottavaa kutsua."</string>
<string name="screen_roomlist_filter_low_priority">"Matala prioriteetti"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Sinulla ei ole vielä yhtään matalan prioriteetin keskustelua"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Voit poistaa suodattimien valinnan nähdäksesi muut keskustelusi."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Sinulla ei ole sopivia keskusteluja tähän valintaan"</string>
<string name="screen_roomlist_filter_people">"Ihmiset"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Sinulla ei ole vielä yhtään yksityisviestiä"</string>
<string name="screen_roomlist_filter_rooms">"Huoneet"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Et ole vielä missään huoneessa"</string>
<string name="screen_roomlist_filter_unreads">"Lukemattomat"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Onnittelut!
Sinulla ei ole lukemattomia viestejä!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Liittymispyyntö lähetetty"</string>
<string name="screen_roomlist_main_space_title">"Keskustelut"</string>
<string name="screen_roomlist_mark_as_read">"Merkitse luetuksi"</string>
<string name="screen_roomlist_mark_as_unread">"Merkitse lukemattomaksi"</string>
<string name="screen_roomlist_tombstoned_room_description">"Tämä huone on päivitetty"</string>
<string name="session_verification_banner_message">"Vaikuttaisi siltä, että käytät uutta laitetta. Vahvista toisella laitteella nähdäksesi salatut viestit."</string>
<string name="session_verification_banner_title">"Vahvista, että se olet sinä"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Désactivez loptimisation de la batterie pour cette application afin de vous assurer que toutes les notifications sont reçues."</string>
<string name="banner_battery_optimization_submit_android">"Désactiver loptimisation"</string>
<string name="banner_battery_optimization_title_android">"Ils vous manque des notifications?"</string>
<string name="banner_new_sound_message">"Le son des notifications a été modifié: plus clair, plus court et moins perturbateur."</string>
<string name="banner_new_sound_title">"Nous avons rafraîchi les sons"</string>
<string name="banner_set_up_recovery_content">"Générez une nouvelle clé de récupération qui peut être utilisée pour restaurer lhistorique de vos messages chiffrés au cas où vous perdriez laccès à vos appareils."</string>
<string name="banner_set_up_recovery_submit">"Configurer la sauvegarde"</string>
<string name="banner_set_up_recovery_title">"Configurer la récupération"</string>
<string name="confirm_recovery_key_banner_message">"Confirmez votre clé de récupération pour conserver laccès à votre stockage de clés et à lhistorique des messages."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Saisissez votre clé de récupération"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Clé de récupération oubliée ?"</string>
<string name="confirm_recovery_key_banner_title">"Le stockage de vos clés nest pas synchronisé"</string>
<string name="full_screen_intent_banner_message">"Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé."</string>
<string name="full_screen_intent_banner_title">"Améliorez votre expérience dappel"</string>
<string name="screen_home_tab_chats">"Conversations"</string>
<string name="screen_home_tab_spaces">"Espaces"</string>
<string name="screen_invites_decline_chat_message">"Êtes-vous sûr de vouloir décliner linvitation à rejoindre %1$s ?"</string>
<string name="screen_invites_decline_chat_title">"Refuser linvitation"</string>
<string name="screen_invites_decline_direct_chat_message">"Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"</string>
<string name="screen_invites_decline_direct_chat_title">"Refuser linvitation"</string>
<string name="screen_invites_empty_list">"Aucune invitation"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) vous a invité(e)"</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_clear_filters">"Supprimer les filtres"</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>
<string name="screen_roomlist_filter_favourites">"Favoris"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Vous pouvez ajouter une discussion aux favoris depuis les paramètres de la discussion.
En attendant, vous pouvez désélectionner des filtres pour voir vos autres salons."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Vous navez pas encore de discussions favorites"</string>
<string name="screen_roomlist_filter_invites">"Invitations"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Vous navez aucune invitation en attente."</string>
<string name="screen_roomlist_filter_low_priority">"Priorité basse"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Vous navez pas encore de salon à priorité basse"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Veuillez désélectionner des filtres pour voir vos discussions"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Vous navez pas de discussions pour cette sélection"</string>
<string name="screen_roomlist_filter_people">"Personnes"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Vous navez pas encore de discussions"</string>
<string name="screen_roomlist_filter_rooms">"Salons"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Vous n’êtes membre daucun salon"</string>
<string name="screen_roomlist_filter_unreads">"Non-lus"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Félicitations !
Vous navez plus de messages non-lus !"</string>
<string name="screen_roomlist_knock_event_sent_description">"Demande de rejoindre le salon envoyée"</string>
<string name="screen_roomlist_main_space_title">"Conversations"</string>
<string name="screen_roomlist_mark_as_read">"Marquer comme lu"</string>
<string name="screen_roomlist_mark_as_unread">"Marquer comme non lu"</string>
<string name="screen_roomlist_tombstoned_room_description">"Ce salon a été mis à niveau."</string>
<string name="session_verification_banner_message">"Il semblerait que vous utilisiez un nouvel appareil. Vérifiez la session avec un autre de vos appareils pour accéder à vos messages chiffrés."</string>
<string name="session_verification_banner_title">"Vérifier que cest bien vous"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Kapcsolja ki az alkalmazás akkumulátoroptimalizálását, hogy biztosan megkapja az összes értesítést."</string>
<string name="banner_battery_optimization_submit_android">"Optimalizálás letiltása"</string>
<string name="banner_battery_optimization_title_android">"Nem érkeznek meg az értesítések?"</string>
<string name="banner_new_sound_message">"Értesítési hangja frissült tisztább, gyorsabb és kevésbé zavaró lett."</string>
<string name="banner_new_sound_title">"Frissítettük a hangokat"</string>
<string name="banner_set_up_recovery_content">"Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést."</string>
<string name="banner_set_up_recovery_submit">"Helyreállítás beállítása"</string>
<string name="banner_set_up_recovery_title">"Helyreállítás beállítása a fiókja védelméhez"</string>
<string name="confirm_recovery_key_banner_message">"Erősítse meg a helyreállítási kulcsát, hogy továbbra is hozzáférjen a kulcstárolójához és az üzenetelőzményekhez."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Adja meg a helyreállítási kulcsot"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Elfelejtette a helyreállítási kulcsot?"</string>
<string name="confirm_recovery_key_banner_title">"A kulcstároló nincs szinkronizálva"</string>
<string name="full_screen_intent_banner_message">"Hogy sose maradjon le egyetlen fontos hívásról sem, a beállításokban engedélyezze a teljes képernyős értesítéseket, amikor a telefon zárolva van."</string>
<string name="full_screen_intent_banner_title">"Fokozza a hívásélményét"</string>
<string name="screen_home_tab_chats">"Összes csevegés"</string>
<string name="screen_home_tab_spaces">"Terek"</string>
<string name="screen_invites_decline_chat_message">"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Meghívás elutasítása"</string>
<string name="screen_invites_decline_direct_chat_message">"Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Csevegés elutasítása"</string>
<string name="screen_invites_empty_list">"Nincsenek meghívások"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) meghívta"</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_clear_filters">"Szűrők törlése"</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_favourites_empty_state_subtitle">"A csevegési beállításokban csevegéseket adhat hozzá a kedvencekhez.
Egyelőre törölheti a szűrőket a többi csevegés megtekintéséhez."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Még nincsenek kedvenc csevegései"</string>
<string name="screen_roomlist_filter_invites">"Meghívások"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Nincsenek függőben lévő meghívásai."</string>
<string name="screen_roomlist_filter_low_priority">"Alacsony prioritás"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Még nincsenek alacsony prioritású csevegései"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Kikapcsolhatja a szűrőket a többi csevegés megtekintéséhez"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Ehhez a kiválasztáshoz nem tartoznak csevegések"</string>
<string name="screen_roomlist_filter_people">"Emberek"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Még nincsenek privát üzenetei"</string>
<string name="screen_roomlist_filter_rooms">"Szobák"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Még nincs egy szobában sem"</string>
<string name="screen_roomlist_filter_unreads">"Olvasatlan"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Gratulálunk!
Nincs olvasatlan üzenete!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Csatlakozási kérés elküldve"</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="screen_roomlist_tombstoned_room_description">"A szoba verzióját frissítették"</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>
</resources>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Nonaktifkan pengoptimalan baterai untuk aplikasi ini, untuk memastikan semua notifikasi diterima."</string>
<string name="banner_battery_optimization_submit_android">"Nonaktifkan optimasi"</string>
<string name="banner_battery_optimization_title_android">"Notifikasi tidak masuk?"</string>
<string name="banner_set_up_recovery_content">"Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda."</string>
<string name="banner_set_up_recovery_submit">"Siapkan pemulihan"</string>
<string name="banner_set_up_recovery_title">"Siapkan pemulihan"</string>
<string name="confirm_recovery_key_banner_message">"Konfirmasikan kunci pemulihan Anda untuk mempertahankan akses ke penyimpanan kunci dan riwayat pesan Anda."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Masukkan kunci pemulihan Anda"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Lupa kunci pemulihan Anda?"</string>
<string name="confirm_recovery_key_banner_title">"Penyimpanan kunci Anda tidak sinkron"</string>
<string name="full_screen_intent_banner_message">"Untuk memastikan Anda tidak melewatkan panggilan penting, silakan ubah pengaturan Anda untuk memperbolehkan notifikasi layar penuh ketika ponsel Anda terkunci."</string>
<string name="full_screen_intent_banner_title">"Tingkatkan pengalaman panggilan Anda"</string>
<string name="screen_home_tab_chats">"Semua Obrolan"</string>
<string name="screen_invites_decline_chat_message">"Apakah Anda yakin ingin menolak undangan untuk bergabung ke %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Tolak undangan"</string>
<string name="screen_invites_decline_direct_chat_message">"Apakah Anda yakin ingin menolak obrolan pribadi dengan %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Tolak obrolan"</string>
<string name="screen_invites_empty_list">"Tidak ada undangan"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) mengundang 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_clear_filters">"Hapus filter"</string>
<string name="screen_roomlist_empty_message">"Mulailah dengan mengirim pesan kepada seseorang."</string>
<string name="screen_roomlist_empty_title">"Belum ada obrolan."</string>
<string name="screen_roomlist_filter_favourites">"Favorit"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Anda dapat menambahkan percakapan ke favorit Anda dalam pengaturan percakapan.
Untuk sementara, Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Anda belum memiliki percakapan favorit"</string>
<string name="screen_roomlist_filter_invites">"Undangan"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Anda tidak memiliki undangan yang tertunda."</string>
<string name="screen_roomlist_filter_low_priority">"Prioritas Rendah"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Anda dapat membatalkan pilihan saringan untuk melihat percakapan Anda yang lain"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Anda tidak memiliki percakapan untuk pemilihan ini"</string>
<string name="screen_roomlist_filter_people">"Orang"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Anda belum memiliki percakapan langsung"</string>
<string name="screen_roomlist_filter_rooms">"Ruangan"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Anda belum berada dalam ruangan"</string>
<string name="screen_roomlist_filter_unreads">"Belum dibaca"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Selamat!
Anda tidak memiliki pesan yang belum dibaca!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Permintaan untuk bergabung dikirim"</string>
<string name="screen_roomlist_main_space_title">"Semua Obrolan"</string>
<string name="screen_roomlist_mark_as_read">"Tandai sebagai dibaca"</string>
<string name="screen_roomlist_mark_as_unread">"Tandai sebagai belum dibaca"</string>
<string name="screen_roomlist_tombstoned_room_description">"Ruangan ini telah ditingkatkan"</string>
<string name="session_verification_banner_message">"Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya."</string>
<string name="session_verification_banner_title">"Verifikasi bahwa ini Anda"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Disabilita l\'ottimizzazione della batteria per questa app, per assicurarti che tutte le notifiche vengano ricevute."</string>
<string name="banner_battery_optimization_submit_android">"Disabilita l\'ottimizzazione"</string>
<string name="banner_battery_optimization_title_android">"Le notifiche non arrivano?"</string>
<string name="banner_new_sound_message">"Il ping delle notifiche è stato aggiornato: ora è più chiaro, più rapido e meno fastidioso."</string>
<string name="banner_new_sound_title">"Abbiamo rinnovato i tuoi suoni"</string>
<string name="banner_set_up_recovery_content">"Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i tuoi dispositivi."</string>
<string name="banner_set_up_recovery_submit">"Configura il recupero"</string>
<string name="banner_set_up_recovery_title">"Configura il ripristino"</string>
<string name="confirm_recovery_key_banner_message">"Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Inserisci la tua chiave di recupero"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Hai dimenticato la chiave di recupero?"</string>
<string name="confirm_recovery_key_banner_title">"L\'archiviazione delle chiavi non è sincronizzata"</string>
<string name="full_screen_intent_banner_message">"Per non perdere mai una chiamata importante, modifica le impostazioni per consentire le notifiche a schermo intero quando il telefono è bloccato."</string>
<string name="full_screen_intent_banner_title">"Migliora la tua esperienza di chiamata"</string>
<string name="screen_home_tab_chats">"Tutte le conversazioni"</string>
<string name="screen_home_tab_spaces">"Spazi"</string>
<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 conversazione privata con %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Rifiuta l\'invito alla conversazione"</string>
<string name="screen_invites_empty_list">"Nessun invito"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) ti ha invitato"</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_clear_filters">"Elimina filtri"</string>
<string name="screen_roomlist_empty_message">"Inizia inviando un messaggio a qualcuno."</string>
<string name="screen_roomlist_empty_title">"Ancora nessuna conversazione."</string>
<string name="screen_roomlist_filter_favourites">"Preferiti"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Puoi aggiungere una conversazione ai tuoi preferiti nelle impostazioni della stessa.
Per il momento, puoi deselezionare i filtri per vedere le altre conversazioni."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Non hai ancora conversazioni preferite"</string>
<string name="screen_roomlist_filter_invites">"Inviti"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Non hai nessun invito in sospeso."</string>
<string name="screen_roomlist_filter_low_priority">"Bassa priorità"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Non hai ancora conversazioni a bassa priorità"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Puoi deselezionare i filtri per vedere le altre conversazioni."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Non hai conversazioni per questa selezione"</string>
<string name="screen_roomlist_filter_people">"Persone"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Non hai ancora nessuna conversazione diretta"</string>
<string name="screen_roomlist_filter_rooms">"Stanze"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Non sei ancora in nessuna stanza"</string>
<string name="screen_roomlist_filter_unreads">"Non letti"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Congratulazioni!
Non hai messaggi non letti!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Richiesta di accesso inviata"</string>
<string name="screen_roomlist_main_space_title">"Tutte le conversazioni"</string>
<string name="screen_roomlist_mark_as_read">"Segna come letto"</string>
<string name="screen_roomlist_mark_as_unread">"Segna come non letto"</string>
<string name="screen_roomlist_tombstoned_room_description">"Questa stanza è stata aggiornata"</string>
<string name="session_verification_banner_message">"Sembra che tu stia usando un nuovo dispositivo. Verificati con un altro dispositivo per accedere ai tuoi messaggi cifrati."</string>
<string name="session_verification_banner_title">"Verifica che sei tu"</string>
</resources>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_set_up_recovery_submit">"აღდგენის დაყენება"</string>
<string name="confirm_recovery_key_banner_message">"დაადასტურეთ თქვენი აღდგენის გასაღები რათა გქონდეთ წვდომა გასაღებების დამგროვებელთან და შეტყობინებების ისტორიასთან."</string>
<string name="confirm_recovery_key_banner_title">"თქვენი გასაღების დამგროვებელი არაა სინქრონიზებული"</string>
<string name="screen_home_tab_chats">"ჩატები"</string>
<string name="screen_invites_decline_chat_message">"დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ მოწვევაზე %1$s-ში?"</string>
<string name="screen_invites_decline_chat_title">"მოწვევაზე უარის თქმა"</string>
<string name="screen_invites_decline_direct_chat_message">"დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ ჩატზე %1$s-თან?"</string>
<string name="screen_invites_decline_direct_chat_title">"ჩატზე უარის თქვა"</string>
<string name="screen_invites_empty_list">"მოწვევები არ არის"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) მოგიწვიათ"</string>
<string name="screen_migration_message">"ეს არის ერთჯერადი პროცესი, მადლობა ლოდინისთვის."</string>
<string name="screen_migration_title">"თქვენი ანგარიშის კონფიგურაცია"</string>
<string name="screen_roomlist_a11y_create_message">"ახალი საუბრისა ან ოთახის შექმნა"</string>
<string name="screen_roomlist_empty_message">"დაიწყეთ ვინმესთვის შეტყობინების გაგზავნით."</string>
<string name="screen_roomlist_empty_title">"არც ერთი ჩატი ჯერ არაა."</string>
<string name="screen_roomlist_filter_favourites">"რჩეულები"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"თქვენ შეგიძლიათ ოთახის რჩეულებში დამატება ოთახების პარამეტრებში.
ახლა კი შეგიძლიათ ფილტრების მოხსნა სხვა ოთახების გამოსაჩენად"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"თქვენ ჯერ არ გაქვთ რჩეული ჩატები"</string>
<string name="screen_roomlist_filter_invites">"მოწვევები"</string>
<string name="screen_roomlist_filter_low_priority">"დაბალი პრიორიტეტი"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"თქვენ შეგიძლიათ წაშალოთ ფილტრები სხვა ჩეთების გამოსაჩენად"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"თქვენ არ გაქვთ ოთახები ამ არჩევნისთვის"</string>
<string name="screen_roomlist_filter_people">"ხალხი"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"თქვენ ჯერ არ გაქვთ პირადი შეტყობინებები"</string>
<string name="screen_roomlist_filter_rooms">"ოთახები"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"თქვენ ჯერ არც ერთ ოთახში არ ხართ"</string>
<string name="screen_roomlist_filter_unreads">"წაუკითხავი"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"გილოცავთ!
თქვენ არ გაქვთ წაუკითხავი შეტყობინებები!"</string>
<string name="screen_roomlist_main_space_title">"ჩატები"</string>
<string name="screen_roomlist_mark_as_read">"წაკითხულად მონიშვნა"</string>
<string name="screen_roomlist_mark_as_unread">"წაუკითხავად მონიშვნა"</string>
<string name="session_verification_banner_message">"როგორც ჩანს, ახალ მოწყობილობას იყენებთ. დაადასტურეთ სხვა მოწყობილობით თქვენს დაშიფრულ შეტყობინებებზე წვდომისთვის."</string>
<string name="session_verification_banner_title">"დაადასტურეთ, რომ ეს თქვენ ხართ"</string>
</resources>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"이 앱의 배터리 최적화를 비활성화하여 모든 알림이 정상적으로 수신되도록 합니다."</string>
<string name="banner_battery_optimization_submit_android">"최적화 비활성화"</string>
<string name="banner_battery_optimization_title_android">"알림이 도착하지 않나요?"</string>
<string name="banner_set_up_recovery_content">"기존의 모든 기기를 분실한 경우 복구 키를 사용하여 암호화된 ID 및 메시지 기록을 복구할 수 있습니다."</string>
<string name="banner_set_up_recovery_submit">"복구 설정"</string>
<string name="banner_set_up_recovery_title">"계정을 보호하기 위해 복구를 설정하세요"</string>
<string name="confirm_recovery_key_banner_message">"키 저장소 및 메시지 기록에 대한 액세스를 유지하려면 복구 키를 확인하세요."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"복구 키를 입력하세요"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"복구 키를 잊으셨나요?"</string>
<string name="confirm_recovery_key_banner_title">"귀하의 키 저장소가 동기화되지 않았습니다"</string>
<string name="full_screen_intent_banner_message">"중요한 전화를 놓치지 않으려면 휴대폰이 잠겨 있을 때 전체 화면 알림을 허용하도록 설정을 변경하세요."</string>
<string name="full_screen_intent_banner_title">"통화 경험을 향상시키세요"</string>
<string name="screen_home_tab_chats">"채팅"</string>
<string name="screen_home_tab_spaces">"스페이스"</string>
<string name="screen_invites_decline_chat_message">"정말로 %1$s 에 참가하지 않고 초대를 거절하시겠어요?"</string>
<string name="screen_invites_decline_chat_title">"초대 거절"</string>
<string name="screen_invites_decline_direct_chat_message">"%1$s 와의 비공개 채팅을 정말 거부하시겠습니까?"</string>
<string name="screen_invites_decline_direct_chat_title">"채팅 거절"</string>
<string name="screen_invites_empty_list">"초대 없음"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) 당신을 초대했습니다"</string>
<string name="screen_migration_message">"이 과정은 한 번만 진행됩니다, 기다려 주셔서 감사합니다."</string>
<string name="screen_migration_title">"계정 설정하기"</string>
<string name="screen_roomlist_a11y_create_message">"새로운 대화 또는 방 만들기"</string>
<string name="screen_roomlist_clear_filters">"필터 지우기"</string>
<string name="screen_roomlist_empty_message">"누군가에게 메시지를 보내어 시작해 보세요."</string>
<string name="screen_roomlist_empty_title">"아직 채팅이 없습니다."</string>
<string name="screen_roomlist_filter_favourites">"즐겨찾기"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"채팅 설정에서 채팅을 즐겨찾기에 추가할 수 있습니다.
현재는 다른 채팅을 보려면 필터를 선택 해제해야 합니다."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"아직 즐겨찾는 채팅이 없습니다."</string>
<string name="screen_roomlist_filter_invites">"초대"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"보류 중인 초대가 없습니다."</string>
<string name="screen_roomlist_filter_low_priority">"낮은 우선순위"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"아직 낮은 우선순위 채팅이 없습니다."</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"다른 채팅을 보려면 필터 선택을 해제하세요."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"이 선택 항목에 대한 채팅이 없습니다."</string>
<string name="screen_roomlist_filter_people">"사람"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"아직 DM이 없습니다."</string>
<string name="screen_roomlist_filter_rooms">"방"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"아직 어떤 방에도 있지 않습니다."</string>
<string name="screen_roomlist_filter_unreads">"읽지 않은 항목"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"축하합니다!
읽지 않은 메시지가 없습니다!"</string>
<string name="screen_roomlist_knock_event_sent_description">"가입 요청이 전송되었습니다"</string>
<string name="screen_roomlist_main_space_title">"채팅"</string>
<string name="screen_roomlist_mark_as_read">"읽음으로 표시"</string>
<string name="screen_roomlist_mark_as_unread">"읽지 않음으로 표시"</string>
<string name="screen_roomlist_tombstoned_room_description">"이 방이 업그레이드되었습니다"</string>
<string name="session_verification_banner_message">"새 장치를 사용 중인 것 같습니다. 다른 디바이스로 인증하여 암호화된 메시지에 액세스하세요."</string>
<string name="session_verification_banner_title">"본인인지 확인하세요"</string>
</resources>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_home_tab_chats">"Pokalbiai"</string>
<string name="screen_invites_decline_chat_message">"Ar tikrai norite atmesti kvietimą prisijungti prie %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Atmesti kvietimą"</string>
<string name="screen_invites_decline_direct_chat_message">"Ar tikrai norite atmesti šį privatų pokalbį su %1$s ?"</string>
<string name="screen_invites_decline_direct_chat_title">"Atmesti pokalbį"</string>
<string name="screen_invites_empty_list">"Jokių kvietimų"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s) pakvietė Jus"</string>
<string name="screen_roomlist_a11y_create_message">"Sukurti naują pokalbį arba kambarį"</string>
<string name="screen_roomlist_filter_invites">"Kvietimai"</string>
<string name="screen_roomlist_filter_people">"Žmonės"</string>
<string name="screen_roomlist_main_space_title">"Pokalbiai"</string>
<string name="session_verification_banner_message">"Panašu, kad naudojate naują įrenginį. Patvirtinkite naudodami kitą įrenginį, kad galėtumėte pasiekti savo šifruotas žinutes."</string>
<string name="session_verification_banner_title">"Patvirtinkite, kad tai Jūs"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Deaktiver batterioptimalisering for denne appen for å sikre at alle varsler mottas."</string>
<string name="banner_battery_optimization_submit_android">"Deaktiver optimalisering"</string>
<string name="banner_battery_optimization_title_android">"Kommer ikke varslene frem?"</string>
<string name="banner_new_sound_message">"Varslingssignalet ditt er oppdatert tydeligere, raskere og mindre forstyrrende."</string>
<string name="banner_new_sound_title">"Vi har oppdatert lydene dine"</string>
<string name="banner_set_up_recovery_content">"Gjenopprett din kryptografiske identitet og meldingshistorikk med en gjenopprettingsnøkkel hvis du har mistet alle dine brukte enheter."</string>
<string name="banner_set_up_recovery_submit">"Konfigurer gjenoppretting"</string>
<string name="banner_set_up_recovery_title">"Konfigurer gjenoppretting for å beskytte kontoen din"</string>
<string name="confirm_recovery_key_banner_message">"Verifiser gjenopprettingsnøkkelen for å opprettholde tilgangen til nøkkellageret og meldingshistorikken."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Skriv inn gjenopprettingsnøkkelen din"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Har du glemt din gjenopprettingsnøkkel?"</string>
<string name="confirm_recovery_key_banner_title">"Nøkkellagringen din er ikke synkronisert"</string>
<string name="full_screen_intent_banner_message">"For å sikre at du aldri går glipp av en viktig samtale, må du endre innstillingene dine for å tillate fullskjermvarsler når telefonen er låst."</string>
<string name="full_screen_intent_banner_title">"Forbedre samtaleopplevelsen din"</string>
<string name="screen_home_tab_chats">"Chatter"</string>
<string name="screen_home_tab_spaces">"Områder"</string>
<string name="screen_invites_decline_chat_message">"Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Avvis invitasjon"</string>
<string name="screen_invites_decline_direct_chat_message">"Er du sikker på at du vil avslå denne private chatten med %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Avslå chat"</string>
<string name="screen_invites_empty_list">"Ingen invitasjoner"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s) inviterte deg"</string>
<string name="screen_migration_message">"Dette er en engangsprosess, takk for at du venter."</string>
<string name="screen_migration_title">"Setter opp kontoen din."</string>
<string name="screen_roomlist_a11y_create_message">"Opprett en ny samtale eller et nytt rom"</string>
<string name="screen_roomlist_clear_filters">"Fjern filtre"</string>
<string name="screen_roomlist_empty_message">"Kom i gang med å sende meldinger til noen."</string>
<string name="screen_roomlist_empty_title">"Ingen chatter ennå."</string>
<string name="screen_roomlist_filter_favourites">"Favoritter"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Du kan legge til en chat blant favorittene dine i chat-innstillingene.
Inntil videre kan du velge bort filtre for å se de andre chattene dine"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Du har ikke favorittchatter ennå"</string>
<string name="screen_roomlist_filter_invites">"Invitasjoner"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Du har ingen ventende invitasjoner."</string>
<string name="screen_roomlist_filter_low_priority">"Lav prioritet"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Du har ingen lavprioriterte chatter ennå"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Du kan velge bort filtre for å se de andre chattene dine"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Du har ikke chatter for dette utvalget"</string>
<string name="screen_roomlist_filter_people">"Personer"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Du har ingen DM-er ennå"</string>
<string name="screen_roomlist_filter_rooms">"Rom"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Du er ikke i noe rom ennå"</string>
<string name="screen_roomlist_filter_unreads">"Uleste"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Gratulerer!
Du har ingen uleste meldinger!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Forespørsel om å bli med sendt"</string>
<string name="screen_roomlist_main_space_title">"Chatter"</string>
<string name="screen_roomlist_mark_as_read">"Marker som lest"</string>
<string name="screen_roomlist_mark_as_unread">"Merk som ulest"</string>
<string name="screen_roomlist_tombstoned_room_description">"Dette rommet har blitt oppgradert"</string>
<string name="session_verification_banner_message">"Det ser ut til at du bruker en ny enhet. Bekreft med en annen enhet for å få tilgang til de krypterte meldingene dine."</string>
<string name="session_verification_banner_title">"Bekreft at det er deg"</string>
</resources>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_submit_android">"Optimalisatie uitschakelen"</string>
<string name="banner_set_up_recovery_content">"Herstel je cryptografische identiteit en berichtengeschiedenis met een herstelsleutel voor als je al je bestaande apparaten kwijt bent."</string>
<string name="banner_set_up_recovery_submit">"Herstelmogelijkheid instellen"</string>
<string name="banner_set_up_recovery_title">"Herstel instellen om je account te beschermen"</string>
<string name="confirm_recovery_key_banner_message">"Bevestig je herstelsleutel om toegang te houden tot je sleutelopslag en berichtengeschiedenis."</string>
<string name="confirm_recovery_key_banner_title">"Je sleutelopslag is niet gesynchroniseerd"</string>
<string name="full_screen_intent_banner_message">"Pas je instellingen aan om meldingen op het volledige scherm toe te staan wanneer de telefoon is vergrendeld. Zo mis je nooit een belangrijk gesprek."</string>
<string name="full_screen_intent_banner_title">"Verbeter je gesprekservaring"</string>
<string name="screen_home_tab_chats">"Chats"</string>
<string name="screen_invites_decline_chat_message">"Weet je zeker dat je de uitnodiging om toe te treden tot %1$s wilt weigeren?"</string>
<string name="screen_invites_decline_chat_title">"Uitnodiging weigeren"</string>
<string name="screen_invites_decline_direct_chat_message">"Weet je zeker dat je deze privéchat met %1$s wilt weigeren?"</string>
<string name="screen_invites_decline_direct_chat_title">"Chat weigeren"</string>
<string name="screen_invites_empty_list">"Geen uitnodigingen"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) heeft je uitgenodigd"</string>
<string name="screen_migration_message">"Dit is een eenmalig proces, bedankt voor het wachten."</string>
<string name="screen_migration_title">"Je account instellen."</string>
<string name="screen_roomlist_a11y_create_message">"Begin een nieuw gesprek of maak een nieuwe kamer"</string>
<string name="screen_roomlist_empty_message">"Ga aan de slag door iemand een bericht te sturen."</string>
<string name="screen_roomlist_empty_title">"Nog geen chats."</string>
<string name="screen_roomlist_filter_favourites">"Favorieten"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Je kunt een chat toevoegen aan je favorieten in de chatinstellingen.
Voor nu kun je filters deselecteren om je andere chats te zien"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Je hebt nog geen favoriete chats"</string>
<string name="screen_roomlist_filter_invites">"Uitnodigingen"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Je hebt geen openstaande uitnodigingen."</string>
<string name="screen_roomlist_filter_low_priority">"Lage prioriteit"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Je kunt filters deselecteren om je andere chats te zien"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Je hebt geen chats voor deze selectie"</string>
<string name="screen_roomlist_filter_people">"Personen"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Je hebt nog geen directe chats"</string>
<string name="screen_roomlist_filter_rooms">"Kamers"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Je zit nog niet in een kamer"</string>
<string name="screen_roomlist_filter_unreads">"Ongelezen"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Gefeliciteerd!
Je hebt geen ongelezen berichten!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Verzoek om toe te treden verzonden"</string>
<string name="screen_roomlist_main_space_title">"Chats"</string>
<string name="screen_roomlist_mark_as_read">"Markeren als gelezen"</string>
<string name="screen_roomlist_mark_as_unread">"Markeren als ongelezen"</string>
<string name="session_verification_banner_message">"Het lijkt erop dat je een nieuw apparaat gebruikt. Verifieer met een ander apparaat om toegang te krijgen tot je versleutelde berichten."</string>
<string name="session_verification_banner_title">"Verifieer dat jij het bent"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Wyłącz optymalizację baterii dla tej aplikacji, aby upewnić się, że wszystkie powiadomienia są odbierane."</string>
<string name="banner_battery_optimization_submit_android">"Wyłącz optymalizację"</string>
<string name="banner_battery_optimization_title_android">"Powiadomienia nie dochodzą?"</string>
<string name="banner_new_sound_message">"Sygnał powiadomień został zaktualizowany — jest wyraźniejszy, szybszy i mniej uciążliwy."</string>
<string name="banner_new_sound_title">"Odświeżyliśmy Twoje dźwięki"</string>
<string name="banner_set_up_recovery_content">"Wygeneruj nowy klucz przywracania, którego można użyć do przywrócenia historii wiadomości szyfrowanych w przypadku utraty dostępu do swoich urządzeń."</string>
<string name="banner_set_up_recovery_submit">"Skonfiguruj przywracanie"</string>
<string name="banner_set_up_recovery_title">"Skonfiguruj przywracanie"</string>
<string name="confirm_recovery_key_banner_message">"Potwierdź klucz przywracania, aby zachować dostęp do magazynu kluczy i historii wiadomości."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Wprowadź klucz przywracania"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Zapomniałeś klucza przywracania?"</string>
<string name="confirm_recovery_key_banner_title">"Magazyn kluczy nie jest zsynchronizowany"</string>
<string name="full_screen_intent_banner_message">"Upewnij się, że nie pominiesz żadnego połączenia. Zmień swoje ustawienia i zezwól na powiadomienia na blokadzie ekranu."</string>
<string name="full_screen_intent_banner_title">"Popraw jakość swoich rozmów"</string>
<string name="screen_home_tab_chats">"Wszystkie czaty"</string>
<string name="screen_home_tab_spaces">"Przestrzenie"</string>
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Odrzuć czat"</string>
<string name="screen_invites_empty_list">"Brak zaproszeń"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) zaprosił Cię"</string>
<string name="screen_migration_message">"Jest to jednorazowy proces, dziękujemy za czekanie."</string>
<string name="screen_migration_title">"Konfigurowanie Twojego konta."</string>
<string name="screen_roomlist_a11y_create_message">"Utwórz nową rozmowę lub pokój"</string>
<string name="screen_roomlist_clear_filters">"Wyczyść filtry"</string>
<string name="screen_roomlist_empty_message">"Wyślij komuś wiadomość, aby rozpocząć."</string>
<string name="screen_roomlist_empty_title">"Brak czatów."</string>
<string name="screen_roomlist_filter_favourites">"Ulubione"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Możesz dodać czat do ulubionych w ustawieniach czatu.
Na razie możesz wyczyścić filtry, aby zobaczyć pozostałe czaty"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Nie masz jeszcze ulubionych czatów"</string>
<string name="screen_roomlist_filter_invites">"Zaproszenia"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Nie masz żadnych oczekujących zaproszeń."</string>
<string name="screen_roomlist_filter_low_priority">"Niski priorytet"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Nie masz jeszcze żadnych czatów o niskim priorytecie"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Wyczyść filtry, aby zobaczyć pozostałe czaty"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Brak czatów dla podanych kryteriów"</string>
<string name="screen_roomlist_filter_people">"Osoby"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Nie masz jeszcze żadnych PW"</string>
<string name="screen_roomlist_filter_rooms">"Pokoje"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Nie jesteś jeszcze w żadnym pokoju"</string>
<string name="screen_roomlist_filter_unreads">"Nieprzeczytane"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Gratulacje!
Nie masz żadnych nieprzeczytanych wiadomości!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Wysłano prośbę o dołączenie"</string>
<string name="screen_roomlist_main_space_title">"Wszystkie czaty"</string>
<string name="screen_roomlist_mark_as_read">"Oznacz jako przeczytane"</string>
<string name="screen_roomlist_mark_as_unread">"Oznacz jako nieprzeczytane"</string>
<string name="screen_roomlist_tombstoned_room_description">"Ten pokój został ulepszony"</string>
<string name="session_verification_banner_message">"Wygląda na to, że używasz nowego urządzenia. Zweryfikuj się innym urządzeniem, aby uzyskać dostęp do zaszyfrowanych wiadomości."</string>
<string name="session_verification_banner_title">"Potwierdź, że to Ty"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Desative a otimização de bateria para este app, para que tenha certeza que todas as notificações sejam recebidas."</string>
<string name="banner_battery_optimization_submit_android">"Desativar otimização"</string>
<string name="banner_battery_optimization_title_android">"As notificações não chegam?"</string>
<string name="banner_new_sound_message">"O seu ping de notificação foi atualizado—mais suave, mais rápido, e menos disruptivo."</string>
<string name="banner_new_sound_title">"Recarregamos seus sons"</string>
<string name="banner_set_up_recovery_content">"Recupere sua identidade criptográfica e o histórico de mensagens com uma chave de recuperação caso você perda todos os dispositivos existentes."</string>
<string name="banner_set_up_recovery_submit">"Configurar a recuperação"</string>
<string name="banner_set_up_recovery_title">"Configure a recuperação para proteger sua conta"</string>
<string name="confirm_recovery_key_banner_message">"Confirme sua chave de recuperação para manter o acesso ao seu armazenamento de chaves e histórico de mensagens."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Digite sua chave de recuperação"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Esqueceu sua chave de recuperação?"</string>
<string name="confirm_recovery_key_banner_title">"Seu armazenamento de chaves está fora de sincronia"</string>
<string name="full_screen_intent_banner_message">"Para garantir que você nunca perca uma chamada importante, por favor altere as suas configurações para permitir notificações em tela cheia enquanto o seu celular estiver bloqueado."</string>
<string name="full_screen_intent_banner_title">"Melhore a sua experiência de chamadas"</string>
<string name="screen_home_tab_chats">"Conversas"</string>
<string name="screen_home_tab_spaces">"Espaços"</string>
<string name="screen_invites_decline_chat_message">"Tem certeza de que deseja recusar o convite para entrar em %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Recusar convite"</string>
<string name="screen_invites_decline_direct_chat_message">"Tem certeza de que deseja recusar esse conversa privada com %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Recusar chat"</string>
<string name="screen_invites_empty_list">"Não há convites"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s) convidou você"</string>
<string name="screen_migration_message">"Este é um processo único, obrigado por esperar."</string>
<string name="screen_migration_title">"Configurando sua conta."</string>
<string name="screen_roomlist_a11y_create_message">"Criar uma nova conversa ou sala"</string>
<string name="screen_roomlist_clear_filters">"Limpar filtros"</string>
<string name="screen_roomlist_empty_message">"Comece enviando uma mensagem para alguém."</string>
<string name="screen_roomlist_empty_title">"Ainda não há conversas."</string>
<string name="screen_roomlist_filter_favourites">"Favoritos"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Você pode adicionar uma conversa aos seus favoritos nas configurações da conversa.
Por enquanto, você pode desmarcar os filtros para ver suas outras conversas"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Você não tem nenhuma conversa favorita ainda"</string>
<string name="screen_roomlist_filter_invites">"Convites"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Você não tem nenhum convite pendente."</string>
<string name="screen_roomlist_filter_low_priority">"Baixa prioridade"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Você ainda não tem nenhuma conversa de baixa prioridade"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Você pode desmarcar filtros para ver suas outras conversas"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Você não tem conversas para esta seleção"</string>
<string name="screen_roomlist_filter_people">"Pessoas"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Você não tem nenhuma conversa privada ainda"</string>
<string name="screen_roomlist_filter_rooms">"Salas"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Você não está em nenhuma sala ainda"</string>
<string name="screen_roomlist_filter_unreads">"Não lidas"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Parabéns!
Você não tem nenhuma mensagem não lida!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Pedido de entrada enviado"</string>
<string name="screen_roomlist_main_space_title">"Conversas"</string>
<string name="screen_roomlist_mark_as_read">"Marcar como lida"</string>
<string name="screen_roomlist_mark_as_unread">"Marcar como não lida"</string>
<string name="screen_roomlist_tombstoned_room_description">"Esta sala foi atualizada"</string>
<string name="session_verification_banner_message">"Parece que você está usando um novo dispositivo. Verifique com outro dispositivo para acessar suas mensagens criptografadas."</string>
<string name="session_verification_banner_title">"Verifique se é você"</string>
</resources>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Desativa as otimizações de bateria para esta aplicação, de modo a garantir que todas as notificações chegam."</string>
<string name="banner_battery_optimization_submit_android">"Desativar otimizações"</string>
<string name="banner_battery_optimization_title_android">"As notificações não chegam?"</string>
<string name="banner_set_up_recovery_content">"Recupera a tua identidade criptográfica e o histórico de mensagens com uma chave de recuperação se tiveres perdido todos os teus dispositivos existentes."</string>
<string name="banner_set_up_recovery_submit">"Configurar recuperação"</string>
<string name="banner_set_up_recovery_title">"Configurar a recuperação"</string>
<string name="confirm_recovery_key_banner_message">"Confirma a tua chave de recuperação para manteres o acesso ao teu armazenamento de chaves e ao histórico de mensagens."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Introduz a tua chave de recuperação"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Esqueceste-te da tua chave de recuperação?"</string>
<string name="confirm_recovery_key_banner_title">"O teu armazenamento de chaves não está sincronizado"</string>
<string name="full_screen_intent_banner_message">"Para garantir que nunca perdes uma chamada importante, altera as configurações para permitir notificações em ecrã inteiro quando o telemóvel está bloqueado."</string>
<string name="full_screen_intent_banner_title">"Melhora a tua experiência de chamada"</string>
<string name="screen_home_tab_chats">"Conversas"</string>
<string name="screen_home_tab_spaces">"Espaços"</string>
<string name="screen_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para entra em %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Rejeitar convite"</string>
<string name="screen_invites_decline_direct_chat_message">"Tens a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Rejeitar conversa"</string>
<string name="screen_invites_empty_list">"Sem convites"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>
<string name="screen_migration_message">"Este processo só acontece uma única vez, obrigado por esperares."</string>
<string name="screen_migration_title">"A configurar a tua conta…"</string>
<string name="screen_roomlist_a11y_create_message">"Criar uma nova conversa ou sala"</string>
<string name="screen_roomlist_clear_filters">"Limpar filtros"</string>
<string name="screen_roomlist_empty_message">"Começa por enviar uma mensagem a alguém."</string>
<string name="screen_roomlist_empty_title">"Ainda não tens conversas."</string>
<string name="screen_roomlist_filter_favourites">"Favoritas"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Podes adicionar uma conversa às tuas favoritas nas suas configurações.
Por enquanto, podes anular a seleção dos filtros para veres as tuas outras conversas"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Ainda não tens nenhuma conversa favorita"</string>
<string name="screen_roomlist_filter_invites">"Convites"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Não tens nenhum convite pendente."</string>
<string name="screen_roomlist_filter_low_priority">"Prioridade baixa"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Ainda não tens conversas de prioridade baixa"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Podes anular a seleção dos filtros para veres as tuas outras conversas"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Não tens nenhuma conversa selecionada"</string>
<string name="screen_roomlist_filter_people">"Pessoas"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Ainda não tens nenhuma MD (mensagem direta)"</string>
<string name="screen_roomlist_filter_rooms">"Salas"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Ainda não estás em nenhuma sala"</string>
<string name="screen_roomlist_filter_unreads">"Por ler"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Parabéns!
Não tens nenhuma mensagem por ler!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Pedido de adesão enviado"</string>
<string name="screen_roomlist_main_space_title">"Conversas"</string>
<string name="screen_roomlist_mark_as_read">"Marcar como lida"</string>
<string name="screen_roomlist_mark_as_unread">"Marcar como não lida"</string>
<string name="screen_roomlist_tombstoned_room_description">"Esta sala foi atualizada"</string>
<string name="session_verification_banner_message">"Parece que estás a utilizar um novo dispositivo. Verifica-o com um outro para poderes aceder às tuas mensagens cifradas."</string>
<string name="session_verification_banner_title">"Verifica que és tu"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Dezactivați optimizarea bateriei pentru această aplicație, pentru a vă asigura că toate notificările sunt primite."</string>
<string name="banner_battery_optimization_submit_android">"Dezactivați optimizarea"</string>
<string name="banner_battery_optimization_title_android">"Nu primiți notificări?"</string>
<string name="banner_new_sound_message">"Sunetul pentru notificări a fost actualizat — mai clar, mai rapid și mai puțin perturbatoar."</string>
<string name="banner_new_sound_title">"Am reîmprospătat sunetele"</string>
<string name="banner_set_up_recovery_content">"Recuperați-vă identitatea criptografică și mesajele anterioare cu o cheie de recuperare dacă ați pierdut toate dispozitivele existente."</string>
<string name="banner_set_up_recovery_submit">"Configurați recuperarea"</string>
<string name="banner_set_up_recovery_title">"Configurați recuperarea pentru a vă proteja contul"</string>
<string name="confirm_recovery_key_banner_message">"Backup-ul pentru chat nu este sincronizat. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Introduceți cheia de recuperare"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Ați uitat cheia de recuperare?"</string>
<string name="confirm_recovery_key_banner_title">"Backup-ul nu este sincronizat"</string>
<string name="full_screen_intent_banner_message">"Pentru a vă asigura că nu pierdeți niciodată un apel important, vă rugăm să modificați setările pentru a permite notificări fullscreen atunci când telefonul este blocat."</string>
<string name="full_screen_intent_banner_title">"Îmbunătățiți-vă experiența in timpul unui apel"</string>
<string name="screen_home_tab_chats">"Toate conversatiile"</string>
<string name="screen_home_tab_spaces">"Spații"</string>
<string name="screen_invites_decline_chat_message">"Sigur doriți să refuzați alăturarea la %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Refuzați invitația"</string>
<string name="screen_invites_decline_direct_chat_message">"Sigur doriți să refuzați conversațiile cu %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Refuzați conversația"</string>
<string name="screen_invites_empty_list">"Nicio invitație"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) v-a invitat."</string>
<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_clear_filters">"Ștergeți filtrele"</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>
<string name="screen_roomlist_filter_favourites">"Favorite"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Puteți adăuga un chat la preferințele dvs. în setările de chat.
Deocamdată, puteți deselecta filtrele pentru a vedea celelalte chat-uri"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Încă nu aveți conversații preferate"</string>
<string name="screen_roomlist_filter_invites">"Invitații"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Nu aveți invitații în așteptare."</string>
<string name="screen_roomlist_filter_low_priority">"Prioritate scăzută"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Nu aveți încă niciun chat cu prioritate scăzută."</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Puteți deselecta filtrele pentru a vedea celelalte chat-uri"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Nu aveți chat-uri pentru această selecție"</string>
<string name="screen_roomlist_filter_people">"Persoane"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Încă nu aveți DM-uri"</string>
<string name="screen_roomlist_filter_rooms">"Camere"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Nu sunteți încă în nicio cameră"</string>
<string name="screen_roomlist_filter_unreads">"Necitite"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Felicitari!
Nu aveți mesaje necitite!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Cererea de alăturare a fost trimisă"</string>
<string name="screen_roomlist_main_space_title">"Toate conversatiile"</string>
<string name="screen_roomlist_mark_as_read">"Marcați ca citită"</string>
<string name="screen_roomlist_mark_as_unread">"Marcați ca necitită"</string>
<string name="screen_roomlist_tombstoned_room_description">"Această cameră a fost modernizată."</string>
<string name="session_verification_banner_message">"Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea cu un alt dispozitiv pentru a accesa mesajele dumneavoastră criptate."</string>
<string name="session_verification_banner_title">"Verificați că sunteți dumneavoastră"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Выключите оптимизацию расхода батареи, чтобы убедиться, что все уведомления будут поступать."</string>
<string name="banner_battery_optimization_submit_android">"Выключить оптимизацию"</string>
<string name="banner_battery_optimization_title_android">"Уведомления не поступают?"</string>
<string name="banner_new_sound_message">"Ваши уведомления были обновлены — теперь они понятнее, быстрее и менее отвлекающие."</string>
<string name="banner_new_sound_title">"Мы обновили ваши звуки"</string>
<string name="banner_set_up_recovery_content">"Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам."</string>
<string name="banner_set_up_recovery_submit">"Настроить восстановление"</string>
<string name="banner_set_up_recovery_title">"Для защиты вашего аккаунта рекомендуется настроить восстановление"</string>
<string name="confirm_recovery_key_banner_message">"Подтвердите ключ восстановления, чтобы сохранить доступ к хранилищу ключей и истории сообщений."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Введите ключ восстановления"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Забыли ключ восстановления?"</string>
<string name="confirm_recovery_key_banner_title">"Хранилище ключей не синхронизировано"</string>
<string name="full_screen_intent_banner_message">"Чтобы больше не пропускать важные звонки, разрешите приложению показывать полноэкранные уведомления на заблокированном экране телефона."</string>
<string name="full_screen_intent_banner_title">"Улучшите качество звонков"</string>
<string name="screen_home_tab_chats">"Все чаты"</string>
<string name="screen_home_tab_spaces">"Пространства"</string>
<string name="screen_invites_decline_chat_message">"Вы уверены, что хотите отклонить приглашение в %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Отклонить приглашение"</string>
<string name="screen_invites_decline_direct_chat_message">"Вы уверены, что хотите отказаться от личного общения с %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Отклонить чат"</string>
<string name="screen_invites_empty_list">"Нет приглашений"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) пригласил вас"</string>
<string name="screen_migration_message">"Это одноразовый процесс, спасибо, что подождали."</string>
<string name="screen_migration_title">"Настройка учетной записи."</string>
<string name="screen_roomlist_a11y_create_message">"Создайте новую беседу или комнату"</string>
<string name="screen_roomlist_clear_filters">"Очистить фильтры"</string>
<string name="screen_roomlist_empty_message">"Начните переписку с отправки сообщения."</string>
<string name="screen_roomlist_empty_title">"Пока нет доступных чатов."</string>
<string name="screen_roomlist_filter_favourites">"Избранное"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Добавить чат в избранное можно в настройках чата.
На данный момент вы можете убрать фильтры, чтобы увидеть другие ваши чаты."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"У вас пока нет избранных чатов"</string>
<string name="screen_roomlist_filter_invites">"Приглашения"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"У вас нет отложенных приглашений."</string>
<string name="screen_roomlist_filter_low_priority">"Низкий приоритет"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"У вас пока нет чатов с низким приоритетом."</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Вы можете убрать фильтры, чтобы увидеть другие ваши чаты."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"У вас нет чатов для этой подборки"</string>
<string name="screen_roomlist_filter_people">"Пользователи"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"У вас пока нет личных сообщений"</string>
<string name="screen_roomlist_filter_rooms">"Комнаты"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Вас пока нет ни в одной комнате"</string>
<string name="screen_roomlist_filter_unreads">"Непрочитанные"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Поздравляем!
Все сообщения прочитаны!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Запрос на присоединение отправлен"</string>
<string name="screen_roomlist_main_space_title">"Все чаты"</string>
<string name="screen_roomlist_mark_as_read">"Пометить как прочитанное"</string>
<string name="screen_roomlist_mark_as_unread">"Отметить как непрочитанное"</string>
<string name="screen_roomlist_tombstoned_room_description">"Эта комната была обновлена"</string>
<string name="session_verification_banner_message">"Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям пройдите подтверждение с другим устройством."</string>
<string name="session_verification_banner_title">"Подтвердите, что это вы"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Vypnite optimalizáciu batérie pre túto aplikáciu, aby ste sa uistili, že sú prijaté všetky upozornenia."</string>
<string name="banner_battery_optimization_submit_android">"Zakázať optimalizáciu"</string>
<string name="banner_battery_optimization_title_android">"Oznámenia neprichádzajú?"</string>
<string name="banner_new_sound_message">"Vaše oznámenia boli aktualizované sú prehľadnejšie, rýchlejšie a menej rušivé."</string>
<string name="banner_new_sound_title">"Obnovili sme vaše zvuky"</string>
<string name="banner_set_up_recovery_content">"Vytvorte nový kľúč na obnovenie, ktorý môžete použiť na obnovenie vašej histórie šifrovaných správ v prípade straty prístupu k vašim zariadeniam."</string>
<string name="banner_set_up_recovery_submit">"Nastaviť obnovenie"</string>
<string name="banner_set_up_recovery_title">"Nastaviť obnovenie"</string>
<string name="confirm_recovery_key_banner_message">"Potvrďte svoj kľúč na obnovenie, aby ste zachovali prístup k úložisku kľúčov a histórii správ."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Zadajte kľúč na obnovenie"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Zabudli ste svoj kľúč na obnovenie?"</string>
<string name="confirm_recovery_key_banner_title">"Vaše úložisko kľúčov nie je synchronizované"</string>
<string name="full_screen_intent_banner_message">"Aby ste už nikdy nezmeškali dôležitý hovor, zmeňte svoje nastavenia a povoľte upozornenia na celú obrazovku, keď je váš telefón uzamknutý."</string>
<string name="full_screen_intent_banner_title">"Vylepšite svoj zážitok z hovoru"</string>
<string name="screen_home_tab_chats">"Všetky konverzácie"</string>
<string name="screen_home_tab_spaces">"Priestory"</string>
<string name="screen_invites_decline_chat_message">"Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odmietnuť pozvanie"</string>
<string name="screen_invites_decline_direct_chat_message">"Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Odmietnuť konverzáciu"</string>
<string name="screen_invites_empty_list">"Žiadne pozvánky"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) vás pozval/a"</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_clear_filters">"Vyčistiť filtre"</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>
<string name="screen_roomlist_filter_favourites">"Obľúbené"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Môžete pridať konverzáciu medzi obľúbené v nastaveniach konverzácie.
Zatiaľ môžete zrušiť výber filtrov, aby ste videli ostatné konverzácie"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Zatiaľ nemáte obľúbené konverzácie"</string>
<string name="screen_roomlist_filter_invites">"Pozvánky"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Nemáte žiadne čakajúce pozvánky."</string>
<string name="screen_roomlist_filter_low_priority">"Nízka priorita"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Zatiaľ nemáte žiadne konverzácie s nízkou prioritou."</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Môžete zrušiť výber filtrov, aby ste videli svoje ostatné konverzácie"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Nemáte konverzácie pre tento výber"</string>
<string name="screen_roomlist_filter_people">"Ľudia"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Zatiaľ nemáte žiadne priame správy"</string>
<string name="screen_roomlist_filter_rooms">"Miestnosti"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Zatiaľ ešte nie ste v žiadnej miestnosti"</string>
<string name="screen_roomlist_filter_unreads">"Neprečítané"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Gratulujeme!
Nemáte žiadne neprečítané správy!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Žiadosť o pripojenie bola odoslaná"</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="screen_roomlist_tombstoned_room_description">"Táto miestnosť bola aktualizovaná"</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>
</resources>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Inaktivera batterioptimering för den här appen för att säkerställa att alla aviseringar tas emot."</string>
<string name="banner_battery_optimization_submit_android">"Inaktivera optimering"</string>
<string name="banner_battery_optimization_title_android">"Aviseringar kommer inte fram?"</string>
<string name="banner_set_up_recovery_content">"Skapa en ny återställningsnyckel som kan användas för att återställa din krypterade meddelandehistorik om du förlorar åtkomst till dina enheter."</string>
<string name="banner_set_up_recovery_submit">"Ställ in återställning"</string>
<string name="banner_set_up_recovery_title">"Ställ in återställning"</string>
<string name="confirm_recovery_key_banner_message">"Bekräfta din återställningsnyckel för att behålla åtkomsten till din nyckellagring och meddelandehistorik."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Ange din återställningsnyckel"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Glömt din återställningsnyckel?"</string>
<string name="confirm_recovery_key_banner_title">"Din nyckellagring är inte synkroniserad"</string>
<string name="full_screen_intent_banner_message">"För att säkerställa att du aldrig missar ett viktigt samtal, ändra dina inställningar för att tillåta helskärmsmeddelanden när telefonen är låst."</string>
<string name="full_screen_intent_banner_title">"Förbättra din samtalsupplevelse"</string>
<string name="screen_home_tab_chats">"Alla chattar"</string>
<string name="screen_home_tab_spaces">"Utrymmen"</string>
<string name="screen_invites_decline_chat_message">"Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?"</string>
<string name="screen_invites_decline_chat_title">"Avböj inbjudan"</string>
<string name="screen_invites_decline_direct_chat_message">"Är du säker på att du vill avböja denna privata chatt med %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Avböj chatt"</string>
<string name="screen_invites_empty_list">"Inga inbjudningar"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) bjöd in dig"</string>
<string name="screen_migration_message">"Detta är en engångsprocess, tack för att du väntar."</string>
<string name="screen_migration_title">"Konfigurerar ditt konto"</string>
<string name="screen_roomlist_a11y_create_message">"Skapa en ny konversation eller ett nytt rum"</string>
<string name="screen_roomlist_clear_filters">"Rensa filter"</string>
<string name="screen_roomlist_empty_message">"Kom igång genom att skicka meddelanden till någon."</string>
<string name="screen_roomlist_empty_title">"Inga chattar än."</string>
<string name="screen_roomlist_filter_favourites">"Favoriter"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Du kan lägga till en chatt till dina favoriter i chattinställningarna.
För tillfället kan du avmarkera filter för att se dina andra chattar"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Du har inga favoritchattar än"</string>
<string name="screen_roomlist_filter_invites">"Inbjudningar"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Du har inga väntande inbjudningar."</string>
<string name="screen_roomlist_filter_low_priority">"Låg prioritet"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Du har inga lågprioriterade chattar ännu"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Du kan avmarkera filter för att se dina andra chattar"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Du har inga chattar för det här valet"</string>
<string name="screen_roomlist_filter_people">"Personer"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Du har inga DM:er än"</string>
<string name="screen_roomlist_filter_rooms">"Rum"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Du är inte i något rum än"</string>
<string name="screen_roomlist_filter_unreads">"Olästa"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Grattis!
Du har inga olästa meddelanden!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Begäran om att gå med skickad"</string>
<string name="screen_roomlist_main_space_title">"Alla chattar"</string>
<string name="screen_roomlist_mark_as_read">"Markera som läst"</string>
<string name="screen_roomlist_mark_as_unread">"Markera som oläst"</string>
<string name="screen_roomlist_tombstoned_room_description">"Det här rummet har uppgraderats"</string>
<string name="session_verification_banner_message">"Det verkar som om du använder en ny enhet. Verifiera med en annan enhet för att komma åt dina krypterade meddelanden."</string>
<string name="session_verification_banner_title">"Verifiera att det är du"</string>
</resources>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_set_up_recovery_content">"Mevcut tüm cihazlarınızı kaybettiyseniz şifreleme kimliğinizi ve mesaj geçmişinizi bir kurtarma anahtarıyla kurtarın."</string>
<string name="banner_set_up_recovery_submit">"Kurtarmayı ayarlayın"</string>
<string name="banner_set_up_recovery_title">"Hesabınızı korumak için kurtarmayı ayarlayın"</string>
<string name="confirm_recovery_key_banner_message">"Anahtar depolama alanınıza ve mesaj geçmişinize erişimi sürdürmek için kurtarma anahtarınızı onaylayın."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Kurtarma anahtarınızı girin"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Kurtarma anahtarınızı mı unuttunuz?"</string>
<string name="confirm_recovery_key_banner_title">"Anahtar depolama alanınız senkronize değil"</string>
<string name="full_screen_intent_banner_message">"Önemli bir aramayı asla kaçırmamak için, telefonunuz kilitliyken tam ekran bildirimlere izin vermek üzere ayarlarınızı değiştirin."</string>
<string name="full_screen_intent_banner_title">"Arama deneyiminizi geliştirin"</string>
<string name="screen_home_tab_chats">"Sohbetler"</string>
<string name="screen_invites_decline_chat_message">"%1$s katılma davetini reddetmek istediğinizden emin misiniz?"</string>
<string name="screen_invites_decline_chat_title">"Daveti reddet"</string>
<string name="screen_invites_decline_direct_chat_message">"%1$s ile bu özel sohbeti reddetmek istediğinizden emin misiniz?"</string>
<string name="screen_invites_decline_direct_chat_title">"Sohbeti reddet"</string>
<string name="screen_invites_empty_list">"Davet Yok"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) sizi davet etti"</string>
<string name="screen_migration_message">"Bu tek seferlik bir işlemdir, beklediğiniz için teşekkürler."</string>
<string name="screen_migration_title">"Hesabınızı ayarlanıyor."</string>
<string name="screen_roomlist_a11y_create_message">"Yeni bir sohbet veya oda oluşturun"</string>
<string name="screen_roomlist_empty_message">"Birine mesaj göndererek başla."</string>
<string name="screen_roomlist_empty_title">"Henüz sohbet yok."</string>
<string name="screen_roomlist_filter_favourites">"Favoriler"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Sohbet ayarlarından bir sohbeti favorilerinize ekleyebilirsiniz.
Şimdilik, diğer sohbetlerinizi görmek için filtrelerin seçimini kaldırabilirsiniz"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Henüz favori sohbetleriniz yok"</string>
<string name="screen_roomlist_filter_invites">"Davetiyeler"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Bekleyen davetiniz yok."</string>
<string name="screen_roomlist_filter_low_priority">"Düşük Öncelikli"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Diğer sohbetlerinizi görmek için filtrelerin seçimini kaldırabilirsiniz"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Bu seçim için sohbetiniz yok"</string>
<string name="screen_roomlist_filter_people">"Kişiler"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Henüz hiç DM\'niz yok"</string>
<string name="screen_roomlist_filter_rooms">"Odalar"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Henüz herhangi bir odada değilsiniz"</string>
<string name="screen_roomlist_filter_unreads">"Okunmamış"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Tebrikler!
Okunmamış mesajınız yok!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Katılma isteği gönderildi"</string>
<string name="screen_roomlist_main_space_title">"Sohbetler"</string>
<string name="screen_roomlist_mark_as_read">"Okundu olarak işaretle"</string>
<string name="screen_roomlist_mark_as_unread">"Okunmamış olarak işaretle"</string>
<string name="session_verification_banner_message">"Görünüşe göre yeni bir cihaz kullanıyorsunuz. Şifrelenmiş mesajlarınıza erişmek için başka bir cihazla doğrulayın."</string>
<string name="session_verification_banner_title">"Siz olduğunuzu doğrulayın"</string>
</resources>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Вимкніть оптимізацію акумулятора для цього застосунку, щоб надходили всі сповіщення."</string>
<string name="banner_battery_optimization_submit_android">"Вимкнути оптимізацію"</string>
<string name="banner_battery_optimization_title_android">"Не надходять сповіщення?"</string>
<string name="banner_set_up_recovery_content">"Відновіть свою криптографічну ідентичність та історію повідомлень за допомогою ключа відновлення, якщо ви втратили всі наявні пристрої."</string>
<string name="banner_set_up_recovery_submit">"Налаштувати відновлення"</string>
<string name="banner_set_up_recovery_title">"Налаштуйте відновлення для захисту свого облікового запису"</string>
<string name="confirm_recovery_key_banner_message">"Підтвердіть свій ключ відновлення, щоб мати доступ до сховища ключів та історії повідомлень."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Введіть ключ відновлення"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Забули ключ відновлення?"</string>
<string name="confirm_recovery_key_banner_title">"Ваше сховище ключів не синхронізовано"</string>
<string name="full_screen_intent_banner_message">"Щоб ніколи не пропустити важливий виклик, змініть налаштування, щоб увімкнути повноекранні сповіщення, коли телефон заблоковано."</string>
<string name="full_screen_intent_banner_title">"Покращуйте досвід дзвінків"</string>
<string name="screen_home_tab_chats">"Бесіди"</string>
<string name="screen_home_tab_spaces">"Простори"</string>
<string name="screen_invites_decline_chat_message">"Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Відхилити запрошення"</string>
<string name="screen_invites_decline_direct_chat_message">"Ви дійсно хочете відмовитися від приватної бесіди з %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Відхилити бесіду"</string>
<string name="screen_invites_empty_list">"Немає запрошень"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) запрошує вас"</string>
<string name="screen_migration_message">"Це одноразовий процес, дякую за очікування."</string>
<string name="screen_migration_title">"Налаштування облікового запису."</string>
<string name="screen_roomlist_a11y_create_message">"Створити нову розмову або кімнату"</string>
<string name="screen_roomlist_clear_filters">"Очистити фільтри"</string>
<string name="screen_roomlist_empty_message">"Почніть з обміну повідомленнями з кимось."</string>
<string name="screen_roomlist_empty_title">"Ще немає бесід."</string>
<string name="screen_roomlist_filter_favourites">"Обране"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Ви можете додати бесіду до обраних у налаштуваннях бесіди.
Наразі ви можете зняти фільтри, щоб побачити інші ваші бесіди"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Ви ще не маєте обраних бесід"</string>
<string name="screen_roomlist_filter_invites">"Запрошення"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"У вас немає запрошень, що очікують на розгляд."</string>
<string name="screen_roomlist_filter_low_priority">"Низький пріоритет"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"У вас ще немає неважливих бесід"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Ви можете зняти фільтри, щоб побачити інші ваші бесіди"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Ви не маєте бесід для цієї категорії"</string>
<string name="screen_roomlist_filter_people">"Люди"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Ви ще не маєте жодної особистої бесіди"</string>
<string name="screen_roomlist_filter_rooms">"Кімнати"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Ви ще не учасник жодної кімнати"</string>
<string name="screen_roomlist_filter_unreads">"Непрочитані"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Вітаємо!
У вас немає непрочитаних повідомлень!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Запит на приєднання надіслано"</string>
<string name="screen_roomlist_main_space_title">"Бесіди"</string>
<string name="screen_roomlist_mark_as_read">"Позначити прочитаним"</string>
<string name="screen_roomlist_mark_as_unread">"Позначити непрочитаним"</string>
<string name="screen_roomlist_tombstoned_room_description">"Цю кімнату оновлено"</string>
<string name="session_verification_banner_message">"Схоже, ви використовуєте новий пристрій. Щоб отримати доступ до зашифрованих повідомлень, підтвердьте особу за допомогою іншого пристрою."</string>
<string name="session_verification_banner_title">"Підтвердьте, що це ви"</string>
</resources>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_set_up_recovery_content">"اگر آپ اپنے تمام موجودہ آلات کھو چکے ہیں تو ایک recovery key کے ذریعہ اپنی کرپٹوگرافک شناخت اور پیغام کی سرگزشت کو دوبارہ حاصل کریں۔"</string>
<string name="banner_set_up_recovery_submit">"بازیابی مرتب کریں"</string>
<string name="banner_set_up_recovery_title">"اپنے اکاؤنٹ کی حفاظت کے لیے ریکوری طے کریں"</string>
<string name="confirm_recovery_key_banner_message">"اپنے کلید کے ذخیرہ اور پیغام کی سرگزشت تک رسائی کو برقرار رکھنے کیلئے اپنی بازیابی کلید کی تصدیق کریں۔"</string>
<string name="confirm_recovery_key_banner_title">"آپ کا کلید کا ذخیرہ غیر ہم وقت ساز ہے۔"</string>
<string name="full_screen_intent_banner_message">"اس بات کو یقینی بنانے کے لیے کہ آپ کبھی بھی اہم مکالمہ سے محروم نہ ہوں، براہ کرم اپنی ترتیبات تبدیل کریں تاکہ آپ کا ہاتف مقفل ہونے پر مکمل پردۂ نمائش اطلاعات کی اجازت دی جا سکے۔"</string>
<string name="full_screen_intent_banner_title">"اپنے مکالمتی تجربے کو احسن کریں"</string>
<string name="screen_home_tab_chats">"گفتگوئیں"</string>
<string name="screen_invites_decline_chat_message">"کیا آپکو یقین ہے کہ آپ %1$s میں شامل ہونے کی درخواست مسترد کرنا چاہتے ہیں؟"</string>
<string name="screen_invites_decline_chat_title">"دعوت مسترد کریں"</string>
<string name="screen_invites_decline_direct_chat_message">"کیا آپکو یقین ہے کہ آپ %1$s کیساتھ نجی گفتگو مسترد کرنا چاہتے ہیں؟"</string>
<string name="screen_invites_decline_direct_chat_title">"گفتگو مسترد کریں"</string>
<string name="screen_invites_empty_list">"کوئی دعوت نامے نہیں"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) نے آپ کو مدعو کیا"</string>
<string name="screen_migration_message">"یہ ایک بار کا عمل ہے، انتظار کرنے کا شکریہ۔"</string>
<string name="screen_migration_title">"آپکا کھاتہ مرتب کر رہا ہے"</string>
<string name="screen_roomlist_a11y_create_message">"ایک نئی گفتگو یا کمرہ تخلیق کریں"</string>
<string name="screen_roomlist_empty_message">"کسی کو پیغام بھیج کر شروع کریں۔"</string>
<string name="screen_roomlist_empty_title">"ابھی تک کوئی گفتگوئیں نہیں ہیں۔"</string>
<string name="screen_roomlist_filter_favourites">"پسندیدگان"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"آپ گفتگو کی ترتیبات میں اپنے پسندیدہ میں گفتگو شامل کر سکتے ہیں۔
ابھی کے لیے، آپ اپنی دوسری گفتگوئیں دیکھنے کے لیے مرشحات کو غیر منتخب کر سکتے ہیں۔"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"آپ کے پاس ابھی تک پسندیدہ گفتگوئیں نہیں ہیں"</string>
<string name="screen_roomlist_filter_invites">"دعوت نامے"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"آپ کے پاس کوئی زیر التوا دعوتیں نہیں ہیں۔"</string>
<string name="screen_roomlist_filter_low_priority">"کم ترجیحی"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"آپ اپنی دیگر گفتگئہں دیکھنے کیلئے مرشحات کو غیر منتخب کرسکتے ہیں"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"آپ کے پاس اس انتخاب کے لیے گفتگو ئیں نہیں ہیں۔"</string>
<string name="screen_roomlist_filter_people">"لوگ"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"آپ کے پاس ابھی تک کوئی براہ راست پیغامات نہیں ہے۔"</string>
<string name="screen_roomlist_filter_rooms">"کمرے"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"آپ ابھی تک کسی کمرے میں نہیں ہیں"</string>
<string name="screen_roomlist_filter_unreads">"غیر مقروءہ"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"مبارک ہو!
آپ کے پاس کوئی غیر مقروءہ پیغامات نہیں!"</string>
<string name="screen_roomlist_main_space_title">"گفتگوئیں"</string>
<string name="screen_roomlist_mark_as_read">"بطور مقروءہ نشانزد کریں"</string>
<string name="screen_roomlist_mark_as_unread">"بطور غیر مقروءہ نشانزد کریں"</string>
<string name="session_verification_banner_message">"ایسا لگتا ہے کہ آپ ایک نیا آلہ استعمال کر رہے ہیں۔ اپنے مرموزکردہ پیغامات تک رسائی کیلئے کسی دوسرے آلے سے توثیق کریں۔"</string>
<string name="session_verification_banner_title">"تصدیق کریں کہ آپ ہی ہیں"</string>
</resources>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Ushbu ilova uchun quvvatni optimallashtirishni oʻchirib qoʻying, barcha xabarnomalar qabul qilinganligiga ishonch hosil qilish uchun."</string>
<string name="banner_battery_optimization_submit_android">"Optimallashtirishni o\'chiring"</string>
<string name="banner_battery_optimization_title_android">"Bildirishnoma kelmayaptimi?"</string>
<string name="banner_set_up_recovery_content">"Mavjud barcha qurilmalarni yoʻqotgan boʻlsangiz, kriptografik kimligingizni va xabarlar tarixini qayta tiklovchi kalit bilan saqlab qoʻying."</string>
<string name="banner_set_up_recovery_submit">"Qayta tiklashni sozlang"</string>
<string name="banner_set_up_recovery_title">"Hisobingizni himoya qilish uchun tiklashni sozlang"</string>
<string name="confirm_recovery_key_banner_message">"Kalit saqlash joyingiz va xabarlar tarixingizga kirishni saqlab qolish uchun tiklash kalitingizni tasdiqlang."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Qayta tiklash kalitingizni kiriting"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Tiklash kalitini unutdingizmi?"</string>
<string name="confirm_recovery_key_banner_title">"Kalit saqlash joyi sinxronlashmagan"</string>
<string name="full_screen_intent_banner_message">"Muhim qoʻngʻiroqlarni oʻtkazib yubormasligingiz uchun telefoningiz qulflangan holatida toʻliq ekranli bildirishnomalarni korsatishga ruxsat beradigan qilib sozlamalaringizni oʻzgartiring."</string>
<string name="full_screen_intent_banner_title">"Qoʻngʻiroq tajribangizni yaxshilang"</string>
<string name="screen_home_tab_chats">"Suhbatlar"</string>
<string name="screen_home_tab_spaces">"Boshliqlar"</string>
<string name="screen_invites_decline_chat_message">"Haqiqatan ham qo\'shilish taklifini rad qilmoqchimisiz%1$s ?"</string>
<string name="screen_invites_decline_chat_title">"Taklifni rad etish"</string>
<string name="screen_invites_decline_direct_chat_message">"Haqiqatan ham bu shaxsiy chatni rad qilmoqchimisiz%1$s ?"</string>
<string name="screen_invites_decline_direct_chat_title">"Chatni rad etish"</string>
<string name="screen_invites_empty_list">"Takliflar yo\'q"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s ) sizni taklif qildi"</string>
<string name="screen_migration_message">"Bu bir martalik jarayon, kutganingiz uchun rahmat."</string>
<string name="screen_migration_title">"Hisobingiz sozlanmoqda."</string>
<string name="screen_roomlist_a11y_create_message">"Yangi suhbat yoki xona yarating"</string>
<string name="screen_roomlist_clear_filters">"Filtrlarni tozalash"</string>
<string name="screen_roomlist_empty_message">"Kimgadir xabar yuborishdan boshlang."</string>
<string name="screen_roomlist_empty_title">"Hozircha chatlar yoq."</string>
<string name="screen_roomlist_filter_favourites">"Sevimlilar"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Siz chat sozlamalarida suhbatni sevimlilar royxatiga qoshishingiz mumkin.
Hozircha, boshqa suhbatlaringizni korish uchun filtrlarni bekor qilishingiz mumkin."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Sizda hali sevimli chatlar yoq"</string>
<string name="screen_roomlist_filter_invites">"Takliflar"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Sizda hech qanday kutilayotgan takliflar yoʻq."</string>
<string name="screen_roomlist_filter_low_priority">"Past darajali"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Sizda hali past ustuvor chatlar yoʻq"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Boshqa suhbatlaringizni koʻrish uchun filtrlarni bekor qilishingiz mumkin"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Sizda bu tanlov uchun chatlar yoq"</string>
<string name="screen_roomlist_filter_people">"Odamlar"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Sizda hali hech qanday shaxsiy xabarlar yoq"</string>
<string name="screen_roomlist_filter_rooms">"Xonalar"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Hali hech qaysi xonada emassiz"</string>
<string name="screen_roomlist_filter_unreads">"Oʻqilmaganlar"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Tabriklaymiz!
Sizda oʻqilmagan xabarlar yoʻq!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Qoshilish sorovi yuborildi"</string>
<string name="screen_roomlist_main_space_title">"Suhbatlar"</string>
<string name="screen_roomlist_mark_as_read">"Oʻqilgan deb belgilash"</string>
<string name="screen_roomlist_mark_as_unread">"Oʻqilmagan deb belgilash"</string>
<string name="screen_roomlist_tombstoned_room_description">"Bu xona yangilandi"</string>
<string name="session_verification_banner_message">"Siz yangi qurilmadan foydalanayotganga oxshaysiz. Shifrlangan xabarlaringizga kirish uchun boshqa qurilma bilan tasdiqlang."</string>
<string name="session_verification_banner_title">"Siz ekanligingizni tasdiqlang"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"停用此應用程式的電池最佳化,才能確保收到所有通知。"</string>
<string name="banner_battery_optimization_submit_android">"停用最佳化"</string>
<string name="banner_battery_optimization_title_android">"沒收到通知?"</string>
<string name="banner_new_sound_message">"您的通知提示音已更新,更清晰、更快、更不易分心。"</string>
<string name="banner_new_sound_title">"我們已更新您的音效設定"</string>
<string name="banner_set_up_recovery_content">"若您遺失了所有現有裝置,則請使用復原金鑰以救援您的密碼學身份與訊息歷史紀錄。"</string>
<string name="banner_set_up_recovery_submit">"設定復原"</string>
<string name="banner_set_up_recovery_title">"設定備援以保護您的帳號"</string>
<string name="confirm_recovery_key_banner_message">"確認您的復原金鑰以維持對金鑰儲存空間與訊息歷史紀錄的存取權。"</string>
<string name="confirm_recovery_key_banner_primary_button_title">"輸入您的復原金鑰"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"忘記了您的復原金鑰?"</string>
<string name="confirm_recovery_key_banner_title">"您的金鑰儲存空間並未同步"</string>
<string name="full_screen_intent_banner_message">"為確保您永遠不會錯過重要通話,請變更設定以允許在手機鎖定時允許全螢幕通知。"</string>
<string name="full_screen_intent_banner_title">"提升您的通話體驗"</string>
<string name="screen_home_tab_chats">"所有聊天室"</string>
<string name="screen_home_tab_spaces">"空間"</string>
<string name="screen_invites_decline_chat_message">"您確定您想要拒絕加入 %1$s 的邀請嗎?"</string>
<string name="screen_invites_decline_chat_title">"拒絕邀請"</string>
<string name="screen_invites_decline_direct_chat_message">"您確定您要拒絕此與 %1$s 的私人聊天嗎?"</string>
<string name="screen_invites_decline_direct_chat_title">"拒絕聊天"</string>
<string name="screen_invites_empty_list">"沒有邀請"</string>
<string name="screen_invites_invited_you">"%1$s%2$s)邀請您"</string>
<string name="screen_migration_message">"這是一次性的程序,感謝您耐心等候。"</string>
<string name="screen_migration_title">"正在設定您的帳號。"</string>
<string name="screen_roomlist_a11y_create_message">"建立新的對話或聊天室"</string>
<string name="screen_roomlist_clear_filters">"清除篩選條件"</string>
<string name="screen_roomlist_empty_message">"從向某人傳送訊息開始。"</string>
<string name="screen_roomlist_empty_title">"尚無聊天室。"</string>
<string name="screen_roomlist_filter_favourites">"我的最愛"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"您可以在聊天設定中將聊天新增至收藏。
目前,您可以取消選取篩選條件以檢視其他聊天"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"您尚無收藏聊天"</string>
<string name="screen_roomlist_filter_invites">"邀請"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"您沒有任何擱置中的邀請。"</string>
<string name="screen_roomlist_filter_low_priority">"低優先度"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"您尚無任何低優先程度聊天"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"您可以取消選取篩選條件以檢視其他聊天"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"您並無此選擇的聊天"</string>
<string name="screen_roomlist_filter_people">"夥伴"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"您尚無任何私人訊息"</string>
<string name="screen_roomlist_filter_rooms">"聊天室"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"您尚未進入任何聊天室"</string>
<string name="screen_roomlist_filter_unreads">"未讀"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"恭喜!
您沒有任何未讀的訊息!"</string>
<string name="screen_roomlist_knock_event_sent_description">"已傳送加入請求"</string>
<string name="screen_roomlist_main_space_title">"所有聊天室"</string>
<string name="screen_roomlist_mark_as_read">"標為已讀"</string>
<string name="screen_roomlist_mark_as_unread">"標為未讀"</string>
<string name="screen_roomlist_tombstoned_room_description">"此聊天室已升級"</string>
<string name="session_verification_banner_message">"您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。"</string>
<string name="session_verification_banner_title">"驗證這是您本人"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"请关闭本应用的电池优化设置,确保不错过任何消息通知。"</string>
<string name="banner_battery_optimization_submit_android">"禁用优化"</string>
<string name="banner_battery_optimization_title_android">"通知未送达?"</string>
<string name="banner_new_sound_message">"您的通知提示音已升级 - 更清晰、更快速、干扰更少。"</string>
<string name="banner_new_sound_title">"我们已更新您的声音"</string>
<string name="banner_set_up_recovery_content">"生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。"</string>
<string name="banner_set_up_recovery_submit">"设置恢复"</string>
<string name="banner_set_up_recovery_title">"设置恢复"</string>
<string name="confirm_recovery_key_banner_message">"确认恢复密钥,以保持对密钥存储和消息历史的访问。"</string>
<string name="confirm_recovery_key_banner_primary_button_title">"输入恢复密钥"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"忘记了恢复密钥?"</string>
<string name="confirm_recovery_key_banner_title">"你的密钥存储已不同步"</string>
<string name="full_screen_intent_banner_message">"为确保您不会错过重要来电,请更改设置以允许锁屏时的全屏通知。"</string>
<string name="full_screen_intent_banner_title">"提升通话体验"</string>
<string name="screen_home_tab_chats">"全部聊天"</string>
<string name="screen_home_tab_spaces">"空间"</string>
<string name="screen_invites_decline_chat_message">"您确定要拒绝加入 %1$s 的邀请吗?"</string>
<string name="screen_invites_decline_chat_title">"拒绝邀请"</string>
<string name="screen_invites_decline_direct_chat_message">"您确定要拒绝与 %1$s 开始私聊吗?"</string>
<string name="screen_invites_decline_direct_chat_title">"拒绝聊天"</string>
<string name="screen_invites_empty_list">"没有邀请"</string>
<string name="screen_invites_invited_you">"%1$s %2$s)邀请了你"</string>
<string name="screen_migration_message">"这是一个一次性的过程,感谢您的等待。"</string>
<string name="screen_migration_title">"设置您的账户。"</string>
<string name="screen_roomlist_a11y_create_message">"创建新的对话或聊天室"</string>
<string name="screen_roomlist_clear_filters">"清除筛选条件"</string>
<string name="screen_roomlist_empty_message">"通过向某人发送消息来开始。"</string>
<string name="screen_roomlist_empty_title">"还没有聊天。"</string>
<string name="screen_roomlist_filter_favourites">"收藏夹"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"可以在聊天设置里将聊天添加到收藏夹中。
现在,可以取消选择过滤器以查看其他对话。"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"您未收藏任何聊天"</string>
<string name="screen_roomlist_filter_invites">"邀请"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"没有待处理的邀请。"</string>
<string name="screen_roomlist_filter_low_priority">"低优先级"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"您还没有任何低优先级聊天"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"您可以取消选择过滤器以查看其他对话"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"您没有关于此选项的聊天"</string>
<string name="screen_roomlist_filter_people">"用户"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"目前您还没有私信"</string>
<string name="screen_roomlist_filter_rooms">"聊天室"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"您尚未进入任何聊天室"</string>
<string name="screen_roomlist_filter_unreads">"未读"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"恭喜!
没有任何未读消息!"</string>
<string name="screen_roomlist_knock_event_sent_description">"加入请求已发送"</string>
<string name="screen_roomlist_main_space_title">"全部聊天"</string>
<string name="screen_roomlist_mark_as_read">"标记为已读"</string>
<string name="screen_roomlist_mark_as_unread">"标记为未读"</string>
<string name="screen_roomlist_tombstoned_room_description">"此房间已升级"</string>
<string name="session_verification_banner_message">"您似乎正在使用新设备。使用另一台设备进行验证以访问您的加密消息。"</string>
<string name="session_verification_banner_title">"验证是你本人"</string>
</resources>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Disable battery optimisation for this app, to make sure all notifications are received."</string>
<string name="banner_battery_optimization_submit_android">"Disable optimisation"</string>
<string name="banner_battery_optimization_title_android">"Notifications not arriving?"</string>
<string name="banner_new_sound_message">"Your notification ping has been updated—clearer, quicker, and less disruptive."</string>
<string name="banner_new_sound_title">"Weve refreshed your sounds"</string>
<string name="banner_set_up_recovery_content">"Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."</string>
<string name="banner_set_up_recovery_submit">"Set up recovery"</string>
<string name="banner_set_up_recovery_title">"Set up recovery to protect your account"</string>
<string name="confirm_recovery_key_banner_message">"Confirm your recovery key to maintain access to your key storage and message history."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Enter your recovery key"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Forgot your recovery key?"</string>
<string name="confirm_recovery_key_banner_title">"Your key storage is out of sync"</string>
<string name="full_screen_intent_banner_message">"To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked."</string>
<string name="full_screen_intent_banner_title">"Enhance your call experience"</string>
<string name="screen_home_tab_chats">"Chats"</string>
<string name="screen_home_tab_spaces">"Spaces"</string>
<string name="screen_invites_decline_chat_message">"Are you sure you want to decline the invitation to join %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Decline invite"</string>
<string name="screen_invites_decline_direct_chat_message">"Are you sure you want to decline this private chat with %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Decline chat"</string>
<string name="screen_invites_empty_list">"No Invites"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</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_clear_filters">"Clear filters"</string>
<string name="screen_roomlist_empty_message">"Get started by messaging someone."</string>
<string name="screen_roomlist_empty_title">"No chats yet."</string>
<string name="screen_roomlist_filter_favourites">"Favourites"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"You can add a chat to your favourites in the chat settings.
For now, you can deselect filters in order to see your other chats"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"You dont have favourite chats yet"</string>
<string name="screen_roomlist_filter_invites">"Invites"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"You don\'t have any pending invites."</string>
<string name="screen_roomlist_filter_low_priority">"Low Priority"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"You dont have any low priority chats yet"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"You can deselect filters in order to see your other chats"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"You dont have chats for this selection"</string>
<string name="screen_roomlist_filter_people">"People"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"You dont have any DMs yet"</string>
<string name="screen_roomlist_filter_rooms">"Rooms"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Youre not in any room yet"</string>
<string name="screen_roomlist_filter_unreads">"Unreads"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Congrats!
You dont have any unread messages!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Request to join sent"</string>
<string name="screen_roomlist_main_space_title">"Chats"</string>
<string name="screen_roomlist_mark_as_read">"Mark as read"</string>
<string name="screen_roomlist_mark_as_unread">"Mark as unread"</string>
<string name="screen_roomlist_tombstoned_room_description">"This room has been upgraded"</string>
<string name="session_verification_banner_message">"Looks like youre using a new device. Verify with another device to access your encrypted messages."</string>
<string name="session_verification_banner_title">"Verify its you"</string>
</resources>
@@ -0,0 +1,223 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.test.aSessionData
import org.junit.Test
class CurrentUserWithNeighborsBuilderTest {
@Test
fun `build on empty list returns current user`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser()
val list = listOf<SessionData>()
val result = sut.build(matrixUser, list)
assertThat(result).containsExactly(matrixUser)
}
@Test
fun `ensure that account are sorted by position`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
position = 3,
),
aSessionData(
sessionId = A_USER_ID_2.value,
position = 2,
),
aSessionData(
sessionId = A_USER_ID_3.value,
position = 1,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID_3,
A_USER_ID_2,
A_USER_ID,
)
}
@Test
fun `if current user is not found, return a singleton with current user`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID_2.value,
),
aSessionData(
sessionId = A_USER_ID_3.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
)
}
@Test
fun `one account, will return a singleton`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
)
}
@Test
fun `two accounts, first is current, will return 3 items`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
aSessionData(
sessionId = A_USER_ID_2.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID_2,
A_USER_ID,
A_USER_ID_2,
)
}
@Test
fun `two accounts, second is current, will return 3 items`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
aSessionData(
sessionId = A_USER_ID_2.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
A_USER_ID_2,
A_USER_ID,
)
}
@Test
fun `three accounts, first is current, will return last current and next`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
aSessionData(
sessionId = A_USER_ID_2.value,
),
aSessionData(
sessionId = A_USER_ID_3.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID_3,
A_USER_ID,
A_USER_ID_2,
)
}
@Test
fun `three accounts, second is current, will return first current and last`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID_2.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
),
aSessionData(
sessionId = A_USER_ID_2.value,
),
aSessionData(
sessionId = A_USER_ID_3.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
A_USER_ID_2,
A_USER_ID_3,
)
}
@Test
fun `three accounts, current is last, will return middle, current and first`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(id = A_USER_ID_3.value)
val list = listOf(
aSessionData(
sessionId = A_USER_ID_2.value,
),
aSessionData(
sessionId = A_USER_ID_3.value,
),
aSessionData(
sessionId = A_USER_ID.value,
),
)
val result = sut.build(matrixUser, list)
assertThat(result.map { it.userId }).containsExactly(
A_USER_ID,
A_USER_ID_2,
A_USER_ID_3,
)
}
@Test
fun `one account, will return data from matrix user and not from db`() {
val sut = CurrentUserWithNeighborsBuilder()
val matrixUser = aMatrixUser(
id = A_USER_ID.value,
displayName = "Bob",
avatarUrl = "avatarUrl",
)
val list = listOf(
aSessionData(
sessionId = A_USER_ID.value,
userDisplayName = "Outdated Bob",
userAvatarUrl = "outdatedAvatarUrl",
),
)
val result = sut.build(matrixUser, list)
assertThat(result).containsExactly(
MatrixUser(
userId = A_USER_ID,
displayName = "Bob",
avatarUrl = "avatarUrl",
)
)
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DefaultHomeEntryPointTest {
@Test
fun `test node builder`() = runTest {
val entryPoint = DefaultHomeEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
HomeFlowNode(
buildContext = buildContext,
plugins = plugins,
matrixClient = FakeMatrixClient(),
presenter = createHomePresenter(),
inviteFriendsUseCase = { lambdaError() },
analyticsService = FakeAnalyticsService(),
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
directLogoutView = { _ -> lambdaError() },
reportRoomEntryPoint = { _, _, _ -> lambdaError() },
declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() },
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
leaveRoomRenderer = { _, _, _ -> lambdaError() },
sessionCoroutineScope = backgroundScope,
)
}
val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) = lambdaError()
override fun navigateToCreateRoom() = lambdaError()
override fun navigateToSettings() = lambdaError()
override fun navigateToSetUpRecovery() = lambdaError()
override fun navigateToEnterRecoveryKey() = lambdaError()
override fun navigateToRoomSettings(roomId: RoomId) = lambdaError()
override fun navigateToBugReport() = lambdaError()
}
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
callback = callback,
)
assertThat(result).isInstanceOf(HomeFlowNode::class.java)
assertThat(result.plugins).contains(callback)
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import io.element.android.libraries.androidutils.system.DateTimeObserver
import kotlinx.coroutines.flow.MutableSharedFlow
class FakeDateTimeObserver : DateTimeObserver {
override val changes = MutableSharedFlow<DateTimeObserver.Event>(extraBufferCapacity = 1)
fun given(event: DateTimeObserver.Event) {
changes.tryEmit(event)
}
}

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