First Commit
This commit is contained in:
@@ -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)
|
||||
}
|
||||
+34
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+70
@@ -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()
|
||||
}
|
||||
}
|
||||
+27
@@ -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))
|
||||
}
|
||||
}
|
||||
+16
@@ -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
|
||||
}
|
||||
+283
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
@@ -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()
|
||||
}
|
||||
+78
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
+18
@@ -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)
|
||||
+46
@@ -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(),
|
||||
)
|
||||
}
|
||||
+46
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+47
@@ -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())
|
||||
}
|
||||
}
|
||||
+332
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+44
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+355
@@ -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),
|
||||
)
|
||||
}
|
||||
+14
@@ -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
|
||||
}
|
||||
+86
@@ -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()
|
||||
}
|
||||
+445
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+45
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+161
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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>
|
||||
}
|
||||
+34
@@ -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>
|
||||
}
|
||||
+32
@@ -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)
|
||||
}
|
||||
}
|
||||
+61
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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
|
||||
}
|
||||
+70
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+27
@@ -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()
|
||||
}
|
||||
}
|
||||
+31
@@ -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,
|
||||
)
|
||||
+220
@@ -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,
|
||||
)
|
||||
}
|
||||
+57
@@ -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()
|
||||
}
|
||||
}
|
||||
+16
@@ -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,
|
||||
)
|
||||
+29
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -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,
|
||||
)
|
||||
}
|
||||
+193
@@ -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
|
||||
)
|
||||
+19
@@ -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,
|
||||
}
|
||||
+58
@@ -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,
|
||||
)
|
||||
+229
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+127
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+34
@@ -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
|
||||
}
|
||||
+344
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -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
|
||||
}
|
||||
+35
@@ -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,
|
||||
)
|
||||
+128
@@ -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()
|
||||
}
|
||||
+64
@@ -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)
|
||||
}
|
||||
}
|
||||
+15
@@ -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
|
||||
}
|
||||
+68
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+19
@@ -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
|
||||
)
|
||||
+39
@@ -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,
|
||||
)
|
||||
+202
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+11
@@ -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
|
||||
+54
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+27
@@ -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
|
||||
}
|
||||
+56
@@ -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")),
|
||||
)
|
||||
}
|
||||
+96
@@ -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,
|
||||
)
|
||||
}
|
||||
+42
@@ -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 don’t 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 l’optimisation 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 l’optimisation"</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 l’historique de vos messages chiffrés au cas où vous perdriez l’accè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 l’accès à votre stockage de clés et à l’historique 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 n’est 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 d’appel"</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 l’invitation à rejoindre %1$s ?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Refuser l’invitation"</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 l’invitation"</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 s’agit d’une opération ponctuelle, merci d’attendre 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 à quelqu’un."</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 n’avez pas encore de discussions favorites"</string>
|
||||
<string name="screen_roomlist_filter_invites">"Invitations"</string>
|
||||
<string name="screen_roomlist_filter_invites_empty_state_title">"Vous n’avez 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 n’avez 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 n’avez 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 n’avez 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 d’aucun salon"</string>
|
||||
<string name="screen_roomlist_filter_unreads">"Non-lus"</string>
|
||||
<string name="screen_roomlist_filter_unreads_empty_state_title">"Félicitations !
|
||||
Vous n’avez 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 c’est 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 ko‘rsatishga 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">"Bo‘shliqlar"</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 yo‘q."</string>
|
||||
<string name="screen_roomlist_filter_favourites">"Sevimlilar"</string>
|
||||
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Siz chat sozlamalarida suhbatni sevimlilar ro‘yxatiga qo‘shishingiz mumkin.
|
||||
Hozircha, boshqa suhbatlaringizni ko‘rish uchun filtrlarni bekor qilishingiz mumkin."</string>
|
||||
<string name="screen_roomlist_filter_favourites_empty_state_title">"Sizda hali sevimli chatlar yo‘q"</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 yo‘q"</string>
|
||||
<string name="screen_roomlist_filter_people">"Odamlar"</string>
|
||||
<string name="screen_roomlist_filter_people_empty_state_title">"Sizda hali hech qanday shaxsiy xabarlar yo‘q"</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">"Qo‘shilish so‘rovi 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 o‘xshaysiz. 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">"We’ve 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 don’t 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 don’t 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 don’t have chats for this selection"</string>
|
||||
<string name="screen_roomlist_filter_people">"People"</string>
|
||||
<string name="screen_roomlist_filter_people_empty_state_title">"You don’t have any DMs yet"</string>
|
||||
<string name="screen_roomlist_filter_rooms">"Rooms"</string>
|
||||
<string name="screen_roomlist_filter_rooms_empty_state_title">"You’re 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 don’t 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 you’re using a new device. Verify with another device to access your encrypted messages."</string>
|
||||
<string name="session_verification_banner_title">"Verify it’s you"</string>
|
||||
</resources>
|
||||
+223
@@ -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",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+64
@@ -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)
|
||||
}
|
||||
}
|
||||
+20
@@ -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
Reference in New Issue
Block a user