First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
import config.BuildTimeConfig
import extension.buildConfigFieldStr
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.preferences.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
buildFeatures {
buildConfig = true
}
defaultConfig {
buildConfigFieldStr(
name = "URL_COPYRIGHT",
value = BuildTimeConfig.URL_COPYRIGHT ?: "https://element.io/copyright",
)
buildConfigFieldStr(
name = "URL_ACCEPTABLE_USE",
value = BuildTimeConfig.URL_ACCEPTABLE_USE ?: "https://element.io/acceptable-use-policy-terms",
)
buildConfigFieldStr(
name = "URL_PRIVACY",
value = BuildTimeConfig.URL_PRIVACY ?: "https://element.io/privacy",
)
}
}
setupDependencyInjection()
dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.appconfig)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.featureflag.ui)
implementation(projects.libraries.network)
implementation(projects.libraries.pushstore.api)
implementation(projects.libraries.indicator.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.troubleshoot.api)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.push.api)
implementation(projects.libraries.pushproviders.api)
implementation(projects.libraries.uiUtils)
implementation(projects.libraries.fullscreenintent.api)
implementation(projects.features.rageshake.api)
implementation(projects.features.lockscreen.api)
implementation(projects.features.analytics.api)
implementation(projects.features.enterprise.api)
implementation(projects.features.licenses.api)
implementation(projects.features.logout.api)
implementation(projects.features.deactivation.api)
implementation(projects.features.home.api)
implementation(projects.features.invite.api)
implementation(projects.services.analytics.api)
implementation(projects.services.analytics.compose)
implementation(projects.services.appnavstate.api)
implementation(projects.services.toolbox.api)
implementation(libs.datetime)
implementation(libs.coil.compose)
implementation(libs.color.picker)
implementation(libs.androidx.browser)
implementation(libs.androidx.datastore.preferences)
api(projects.features.preferences.api)
implementation(libs.showkase)
implementation(platform(libs.network.okhttp.bom))
implementation(libs.network.okhttp)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.roomselect.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.features.deactivation.test)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.features.invite.test)
testImplementation(projects.features.licenses.test)
testImplementation(projects.features.lockscreen.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.logout.test)
testImplementation(projects.libraries.indicator.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.services.appnavstate.impl)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)
}
@@ -0,0 +1,28 @@
/*
* 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.preferences.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.preferences.api.CacheService
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultCacheService : CacheService {
private val _clearedCacheEventFlow = MutableSharedFlow<SessionId>(0)
override val clearedCacheEventFlow: Flow<SessionId> = _clearedCacheEventFlow
suspend fun onClearedCache(sessionId: SessionId) {
_clearedCacheEventFlow.emit(sessionId)
}
}
@@ -0,0 +1,37 @@
/*
* 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.preferences.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.preferences.api.PreferencesEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultPreferencesEntryPoint : PreferencesEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: PreferencesEntryPoint.Params,
callback: PreferencesEntryPoint.Callback,
): Node {
return parentNode.createNode<PreferencesFlowNode>(
buildContext = buildContext,
plugins = listOf(params, callback)
)
}
}
internal fun PreferencesEntryPoint.InitialTarget.toNavTarget() = when (this) {
is PreferencesEntryPoint.InitialTarget.Root -> PreferencesFlowNode.NavTarget.Root
is PreferencesEntryPoint.InitialTarget.NotificationSettings -> PreferencesFlowNode.NavTarget.NotificationSettings
PreferencesEntryPoint.InitialTarget.NotificationTroubleshoot -> PreferencesFlowNode.NavTarget.TroubleshootNotifications
}
@@ -0,0 +1,326 @@
/*
* 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.preferences.impl
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
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 io.element.android.annotations.ContributesNode
import io.element.android.features.deactivation.api.AccountDeactivationEntryPoint
import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.preferences.impl.about.AboutNode
import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNode
import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode
import io.element.android.features.preferences.impl.blockedusers.BlockedUsersNode
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
import io.element.android.features.preferences.impl.labs.LabsNode
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode
import io.element.android.features.preferences.impl.user.editprofile.EditUserProfileNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.appyx.canPop
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@AssistedInject
class PreferencesFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val lockScreenEntryPoint: LockScreenEntryPoint,
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
private val pushHistoryEntryPoint: PushHistoryEntryPoint,
private val logoutEntryPoint: LogoutEntryPoint,
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
private val accountDeactivationEntryPoint: AccountDeactivationEntryPoint,
) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget
@Parcelize
data object DeveloperSettings : NavTarget
@Parcelize
data object AdvancedSettings : NavTarget
@Parcelize
data object Labs : NavTarget
@Parcelize
data object AnalyticsSettings : NavTarget
@Parcelize
data object About : NavTarget
@Parcelize
data object NotificationSettings : NavTarget
@Parcelize
data object TroubleshootNotifications : NavTarget
@Parcelize
data object PushHistory : NavTarget
@Parcelize
data object LockScreenSettings : NavTarget
@Parcelize
data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget
@Parcelize
data class UserProfile(val matrixUser: MatrixUser) : NavTarget
@Parcelize
data object BlockedUsers : NavTarget
@Parcelize
data object SignOut : NavTarget
@Parcelize
data object AccountDeactivation : NavTarget
@Parcelize
data object OssLicenses : NavTarget
}
private val callback: PreferencesEntryPoint.Callback = callback()
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Root -> {
val callback = object : PreferencesRootNode.Callback {
override fun navigateToAddAccount() {
callback.navigateToAddAccount()
}
override fun navigateToBugReport() {
callback.navigateToBugReport()
}
override fun navigateToSecureBackup() {
callback.navigateToSecureBackup()
}
override fun navigateToAnalyticsSettings() {
backstack.push(NavTarget.AnalyticsSettings)
}
override fun navigateToAbout() {
backstack.push(NavTarget.About)
}
override fun navigateToDeveloperSettings() {
backstack.push(NavTarget.DeveloperSettings)
}
override fun navigateToNotificationSettings() {
backstack.push(NavTarget.NotificationSettings)
}
override fun navigateToLockScreenSettings() {
backstack.push(NavTarget.LockScreenSettings)
}
override fun navigateToAdvancedSettings() {
backstack.push(NavTarget.AdvancedSettings)
}
override fun navigateToLabs() {
backstack.push(NavTarget.Labs)
}
override fun navigateToUserProfile(matrixUser: MatrixUser) {
backstack.push(NavTarget.UserProfile(matrixUser))
}
override fun navigateToBlockedUsers() {
backstack.push(NavTarget.BlockedUsers)
}
override fun startSignOutFlow() {
backstack.push(NavTarget.SignOut)
}
override fun startAccountDeactivationFlow() {
backstack.push(NavTarget.AccountDeactivation)
}
}
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
}
NavTarget.DeveloperSettings -> {
val developerSettingsCallback = object : DeveloperSettingsNode.Callback {
override fun navigateToPushHistory() {
backstack.push(NavTarget.PushHistory)
}
override fun onDone() {
backstack.pop()
}
}
createNode<DeveloperSettingsNode>(buildContext, listOf(developerSettingsCallback))
}
NavTarget.Labs -> {
val callback = object : LabsNode.Callback {
override fun onDone() {
backstack.pop()
}
}
createNode<LabsNode>(buildContext, listOf(callback))
}
NavTarget.About -> {
val callback = object : AboutNode.Callback {
override fun navigateToOssLicenses() {
backstack.push(NavTarget.OssLicenses)
}
}
createNode<AboutNode>(buildContext, listOf(callback))
}
NavTarget.AnalyticsSettings -> {
createNode<AnalyticsSettingsNode>(buildContext)
}
NavTarget.NotificationSettings -> {
val notificationSettingsCallback = object : NotificationSettingsNode.Callback {
override fun navigateToEditDefaultNotificationSetting(isOneToOne: Boolean) {
backstack.push(NavTarget.EditDefaultNotificationSetting(isOneToOne))
}
override fun navigateToTroubleshootNotifications() {
backstack.push(NavTarget.TroubleshootNotifications)
}
}
createNode<NotificationSettingsNode>(buildContext, listOf(notificationSettingsCallback))
}
NavTarget.TroubleshootNotifications -> {
notificationTroubleShootEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
callback = object : NotificationTroubleShootEntryPoint.Callback {
override fun onDone() {
if (backstack.canPop()) {
backstack.pop()
} else {
navigateUp()
}
}
override fun navigateToBlockedUsers() {
backstack.push(NavTarget.BlockedUsers)
}
},
)
}
NavTarget.PushHistory -> {
pushHistoryEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
callback = object : PushHistoryEntryPoint.Callback {
override fun onDone() {
if (backstack.canPop()) {
backstack.pop()
} else {
navigateUp()
}
}
override fun navigateToEvent(roomId: RoomId, eventId: EventId) {
callback.navigateToEvent(roomId, eventId)
}
},
)
}
is NavTarget.EditDefaultNotificationSetting -> {
val callback = object : EditDefaultNotificationSettingNode.Callback {
override fun navigateToRoomNotificationSettings(roomId: RoomId) {
callback.navigateToRoomNotificationSettings(roomId)
}
}
val input = EditDefaultNotificationSettingNode.Inputs(navTarget.isOneToOne)
createNode<EditDefaultNotificationSettingNode>(buildContext, plugins = listOf(input, callback))
}
NavTarget.AdvancedSettings -> {
createNode<AdvancedSettingsNode>(buildContext)
}
is NavTarget.UserProfile -> {
val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser)
val callback = object : EditUserProfileNode.Callback {
override fun onDone() {
backstack.pop()
}
}
createNode<EditUserProfileNode>(buildContext, listOf(inputs, callback))
}
NavTarget.LockScreenSettings -> {
lockScreenEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
navTarget = LockScreenEntryPoint.Target.Settings,
callback = object : LockScreenEntryPoint.Callback {
override fun onSetupDone() {
// No op
}
}
)
}
NavTarget.BlockedUsers -> {
createNode<BlockedUsersNode>(buildContext)
}
NavTarget.SignOut -> {
val callBack: LogoutEntryPoint.Callback = object : LogoutEntryPoint.Callback {
override fun navigateToSecureBackup() {
callback.navigateToSecureBackup()
}
}
logoutEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
callback = callBack,
)
}
is NavTarget.OssLicenses -> {
openSourceLicensesEntryPoint.createNode(this, buildContext)
}
NavTarget.AccountDeactivation -> {
accountDeactivationEntryPoint.createNode(this, buildContext)
}
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}
@@ -0,0 +1,62 @@
/*
* 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.preferences.impl.about
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class AboutNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: AboutPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToOssLicenses()
}
private val callback: Callback = callback()
private fun onElementLegalClick(
activity: Activity,
darkTheme: Boolean,
elementLegal: ElementLegal,
) {
activity.openUrlInChromeCustomTab(null, darkTheme, elementLegal.url)
}
@Composable
override fun View(modifier: Modifier) {
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
val state = presenter.present()
AboutView(
state = state,
onBackClick = ::navigateUp,
onElementLegalClick = { elementLegal ->
onElementLegalClick(activity, isDark, elementLegal)
},
onOpenSourceLicensesClick = callback::navigateToOssLicenses,
modifier = modifier
)
}
}
@@ -0,0 +1,23 @@
/*
* 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.preferences.impl.about
import androidx.compose.runtime.Composable
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
@Inject
class AboutPresenter : Presenter<AboutState> {
@Composable
override fun present(): AboutState {
return AboutState(
elementLegals = getAllLegals(),
)
}
}
@@ -0,0 +1,15 @@
/*
* 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.preferences.impl.about
import kotlinx.collections.immutable.ImmutableList
data class AboutState(
val elementLegals: ImmutableList<ElementLegal>,
)
@@ -0,0 +1,25 @@
/*
* 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.preferences.impl.about
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import kotlinx.collections.immutable.toImmutableList
open class AboutStateProvider : PreviewParameterProvider<AboutState> {
override val values: Sequence<AboutState>
get() = sequenceOf(
anAboutState(),
)
}
fun anAboutState(
elementLegals: List<ElementLegal> = getAllLegals(),
) = AboutState(
elementLegals = elementLegals.toImmutableList(),
)
@@ -0,0 +1,61 @@
/*
* 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.preferences.impl.about
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AboutView(
state: AboutState,
onElementLegalClick: (ElementLegal) -> Unit,
onOpenSourceLicensesClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferencePage(
modifier = modifier,
onBackClick = onBackClick,
title = stringResource(id = CommonStrings.common_about)
) {
state.elementLegals.forEach { elementLegal ->
ListItem(
headlineContent = {
Text(stringResource(id = elementLegal.titleRes))
},
onClick = { onElementLegalClick(elementLegal) }
)
}
ListItem(
headlineContent = {
Text(stringResource(id = CommonStrings.common_open_source_licenses))
},
onClick = onOpenSourceLicensesClick,
)
}
}
@PreviewsDayNight
@Composable
internal fun AboutViewPreview(@PreviewParameter(AboutStateProvider::class) state: AboutState) = ElementPreview {
AboutView(
state = state,
onElementLegalClick = {},
onOpenSourceLicensesClick = {},
onBackClick = {},
)
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2021-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.preferences.impl.about
import androidx.annotation.StringRes
import io.element.android.features.preferences.impl.BuildConfig
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
private const val COPYRIGHT_URL = BuildConfig.URL_COPYRIGHT
private const val USE_POLICY_URL = BuildConfig.URL_ACCEPTABLE_USE
private const val PRIVACY_URL = BuildConfig.URL_PRIVACY
sealed class ElementLegal(
@StringRes val titleRes: Int,
val url: String,
) {
data object Copyright : ElementLegal(CommonStrings.common_copyright, COPYRIGHT_URL)
data object AcceptableUsePolicy : ElementLegal(CommonStrings.common_acceptable_use_policy, USE_POLICY_URL)
data object PrivacyPolicy : ElementLegal(CommonStrings.common_privacy_policy, PRIVACY_URL)
}
fun getAllLegals(): ImmutableList<ElementLegal> {
return persistentListOf(
ElementLegal.Copyright,
ElementLegal.AcceptableUsePolicy,
ElementLegal.PrivacyPolicy,
)
}
@@ -0,0 +1,23 @@
/*
* 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.preferences.impl.advanced
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
sealed interface AdvancedSettingsEvents {
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvents
data class SetCompressImages(val compress: Boolean) : AdvancedSettingsEvents
data class SetVideoUploadQuality(val videoPreset: VideoCompressionPreset) : AdvancedSettingsEvents
data class SetTheme(val theme: ThemeOption) : AdvancedSettingsEvents
data class SetTimelineMediaPreviewValue(val value: MediaPreviewValue) : AdvancedSettingsEvents
data class SetHideInviteAvatars(val value: Boolean) : AdvancedSettingsEvents
}
@@ -0,0 +1,37 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class AdvancedSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: AdvancedSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
AdvancedSettingsView(
state = state,
modifier = modifier,
onBackClick = ::navigateUp
)
}
}
@@ -0,0 +1,124 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.mapToTheme
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@Inject
class AdvancedSettingsPresenter(
private val appPreferencesStore: AppPreferencesStore,
private val sessionPreferencesStore: SessionPreferencesStore,
private val mediaPreviewConfigStateStore: MediaPreviewConfigStateStore,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val featureFlagService: FeatureFlagService,
) : Presenter<AdvancedSettingsState> {
@Composable
override fun present(): AdvancedSettingsState {
val isDeveloperModeEnabled by remember {
appPreferencesStore.isDeveloperModeEnabledFlow()
}.collectAsState(initial = false)
val isSharePresenceEnabled by remember {
sessionPreferencesStore.isSharePresenceEnabled()
}.collectAsState(initial = true)
val theme = remember {
appPreferencesStore.getThemeFlow().mapToTheme()
}.collectAsState(initial = Theme.System)
val mediaPreviewConfigState = mediaPreviewConfigStateStore.state()
val themeOption by remember {
derivedStateOf {
when (theme.value) {
Theme.System -> ThemeOption.System
Theme.Dark -> ThemeOption.Dark
Theme.Light -> ThemeOption.Light
}
}
}
val hasSplitMediaQualityOptions by produceState<Boolean?>(null) {
value = featureFlagService.isFeatureEnabled(FeatureFlags.SelectableMediaQuality)
}
val mediaOptimizationState by produceState<MediaOptimizationState?>(null) {
val hasSplitMediaQualityOptionsFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.SelectableMediaQuality)
combine(
hasSplitMediaQualityOptionsFlow,
sessionPreferencesStore.doesOptimizeImages(),
sessionPreferencesStore.getVideoCompressionPreset()
) { hasSplitOptions, compressImages, videoPreset ->
if (hasSplitMediaQualityOptions == true) {
value = MediaOptimizationState.Split(
compressImages = compressImages,
videoPreset = videoPreset,
)
} else if (hasSplitMediaQualityOptions == false) {
value = MediaOptimizationState.AllMedia(isEnabled = compressImages)
}
}.collect()
}
fun handleEvent(event: AdvancedSettingsEvents) {
when (event) {
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> sessionCoroutineScope.launch {
appPreferencesStore.setDeveloperModeEnabled(event.enabled)
}
is AdvancedSettingsEvents.SetSharePresenceEnabled -> sessionCoroutineScope.launch {
sessionPreferencesStore.setSharePresence(event.enabled)
}
is AdvancedSettingsEvents.SetCompressMedia -> sessionCoroutineScope.launch {
sessionPreferencesStore.setOptimizeImages(event.compress)
}
is AdvancedSettingsEvents.SetTheme -> sessionCoroutineScope.launch {
when (event.theme) {
ThemeOption.System -> appPreferencesStore.setTheme(Theme.System.name)
ThemeOption.Dark -> appPreferencesStore.setTheme(Theme.Dark.name)
ThemeOption.Light -> appPreferencesStore.setTheme(Theme.Light.name)
}
}
is AdvancedSettingsEvents.SetHideInviteAvatars -> mediaPreviewConfigStateStore.setHideInviteAvatars(event.value)
is AdvancedSettingsEvents.SetTimelineMediaPreviewValue -> mediaPreviewConfigStateStore.setTimelineMediaPreviewValue(event.value)
is AdvancedSettingsEvents.SetCompressImages -> sessionCoroutineScope.launch {
sessionPreferencesStore.setOptimizeImages(event.compress)
}
is AdvancedSettingsEvents.SetVideoUploadQuality -> sessionCoroutineScope.launch {
sessionPreferencesStore.setVideoCompressionPreset(event.videoPreset)
}
}
}
return AdvancedSettingsState(
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSharePresenceEnabled = isSharePresenceEnabled,
mediaOptimizationState = mediaOptimizationState,
theme = themeOption,
mediaPreviewConfigState = mediaPreviewConfigState,
eventSink = ::handleEvent,
)
}
}
@@ -0,0 +1,56 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.preferences.DropdownOption
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.libraries.ui.strings.CommonStrings
data class AdvancedSettingsState(
val isDeveloperModeEnabled: Boolean,
val isSharePresenceEnabled: Boolean,
val mediaOptimizationState: MediaOptimizationState?,
val theme: ThemeOption,
val mediaPreviewConfigState: MediaPreviewConfigState,
val eventSink: (AdvancedSettingsEvents) -> Unit
)
sealed interface MediaOptimizationState {
data class AllMedia(val isEnabled: Boolean) : MediaOptimizationState
data class Split(
val compressImages: Boolean,
val videoPreset: VideoCompressionPreset,
) : MediaOptimizationState
val shouldCompressImages: Boolean get() = when (this) {
is AllMedia -> isEnabled
is Split -> compressImages
}
}
enum class ThemeOption : DropdownOption {
System {
@Composable
@ReadOnlyComposable
override fun getText(): String = stringResource(CommonStrings.common_system)
},
Dark {
@Composable
@ReadOnlyComposable
override fun getText(): String = stringResource(CommonStrings.common_dark)
},
Light {
@Composable
@ReadOnlyComposable
override fun getText(): String = stringResource(CommonStrings.common_light)
}
}
@@ -0,0 +1,56 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
override val values: Sequence<AdvancedSettingsState>
get() = sequenceOf(
aAdvancedSettingsState(),
aAdvancedSettingsState(isDeveloperModeEnabled = true),
aAdvancedSettingsState(isSharePresenceEnabled = true),
aAdvancedSettingsState(mediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = true)),
aAdvancedSettingsState(hideInviteAvatars = true),
aAdvancedSettingsState(timelineMediaPreviewValue = MediaPreviewValue.Off),
aAdvancedSettingsState(setHideInviteAvatarsAction = AsyncAction.Loading),
aAdvancedSettingsState(setTimelineMediaPreviewAction = AsyncAction.Loading),
aAdvancedSettingsState(mediaOptimizationState = MediaOptimizationState.Split(
compressImages = true,
videoPreset = VideoCompressionPreset.HIGH,
)),
)
}
fun aAdvancedSettingsState(
isDeveloperModeEnabled: Boolean = false,
isSharePresenceEnabled: Boolean = false,
mediaOptimizationState: MediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = false),
theme: ThemeOption = ThemeOption.System,
hideInviteAvatars: Boolean = false,
timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On,
setTimelineMediaPreviewAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
setHideInviteAvatarsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (AdvancedSettingsEvents) -> Unit = {},
) = AdvancedSettingsState(
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSharePresenceEnabled = isSharePresenceEnabled,
mediaOptimizationState = mediaOptimizationState,
theme = theme,
mediaPreviewConfigState = MediaPreviewConfigState(
hideInviteAvatars = hideInviteAvatars,
timelineMediaPreviewValue = timelineMediaPreviewValue,
setTimelineMediaPreviewAction = setTimelineMediaPreviewAction,
setHideInviteAvatarsAction = setHideInviteAvatarsAction
),
eventSink = eventSink
)
@@ -0,0 +1,346 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.components.dialogs.ListDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
import io.element.android.libraries.designsystem.theme.components.ListSupportingTextDefaults
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.toImmutableList
@Composable
fun AdvancedSettingsView(
state: AdvancedSettingsState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val analyticsService = LocalAnalyticsService.current
val snackbarDispatcher = LocalSnackbarDispatcher.current
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = snackbarMessage)
PreferencePage(
modifier = modifier,
onBackClick = onBackClick,
title = stringResource(id = CommonStrings.common_advanced_settings),
snackbarHost = {
SnackbarHost(
snackbarHostState,
modifier = Modifier.navigationBarsPadding()
)
}
) {
PreferenceDropdown(
title = stringResource(id = CommonStrings.common_appearance),
selectedOption = state.theme,
options = ThemeOption.entries.toImmutableList(),
onSelectOption = { themeOption ->
state.eventSink(AdvancedSettingsEvents.SetTheme(themeOption))
}
)
ListItem(
headlineContent = {
Text(text = stringResource(id = CommonStrings.action_view_source))
},
supportingContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_view_source_description))
},
trailingContent = ListItemContent.Switch(
checked = state.isDeveloperModeEnabled,
),
onClick = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(!state.isDeveloperModeEnabled)) }
)
ListItem(
headlineContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_share_presence))
},
supportingContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_share_presence_description))
},
trailingContent = ListItemContent.Switch(
checked = state.isSharePresenceEnabled,
),
onClick = { state.eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) }
)
val compressImages = state.mediaOptimizationState?.shouldCompressImages
when (state.mediaOptimizationState) {
null -> Unit
is MediaOptimizationState.AllMedia -> {
ListItem(
headlineContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_title))
},
supportingContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_description))
},
trailingContent = ListItemContent.Switch(
checked = compressImages ?: false,
),
onClick = {
val newValue = !(compressImages ?: false)
analyticsService.captureInteraction(
if (newValue) {
Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
} else {
Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
}
)
state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue))
}
)
}
is MediaOptimizationState.Split -> {
ListItem(
headlineContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_optimise_image_upload_quality_title))
},
supportingContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_optimise_image_upload_quality_description))
},
trailingContent = ListItemContent.Switch(
checked = compressImages ?: false,
),
onClick = {
val newValue = !(compressImages ?: false)
analyticsService.captureInteraction(
if (newValue) {
Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
} else {
Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
}
)
state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue))
}
)
var displaySelectorDialog by remember { mutableStateOf(false) }
ListItem(
headlineContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_optimise_video_upload_quality_title))
},
supportingContent = {
val description = stringResource(id = R.string.screen_advanced_settings_optimise_video_upload_quality_description)
val quality = when (state.mediaOptimizationState.videoPreset) {
VideoCompressionPreset.LOW -> stringResource(id = R.string.screen_advanced_settings_optimise_video_upload_quality_low)
VideoCompressionPreset.STANDARD -> stringResource(id = R.string.screen_advanced_settings_optimise_video_upload_quality_standard)
VideoCompressionPreset.HIGH -> stringResource(id = R.string.screen_advanced_settings_optimise_video_upload_quality_high)
}
val descriptionWithValue = remember(quality) {
String.format(description, quality)
}
Text(text = descriptionWithValue)
},
onClick = { displaySelectorDialog = true },
)
if (displaySelectorDialog) {
VideoQualitySelectorDialog(
selectedPreset = state.mediaOptimizationState.videoPreset,
onSubmit = { preset ->
state.eventSink(AdvancedSettingsEvents.SetVideoUploadQuality(preset))
displaySelectorDialog = false
},
onDismiss = { displaySelectorDialog = false },
)
}
}
}
ModerationAndSafety(state)
}
}
@Composable
private fun VideoQualitySelectorDialog(
selectedPreset: VideoCompressionPreset,
onSubmit: (VideoCompressionPreset) -> Unit,
onDismiss: () -> Unit
) {
val videoPresets = VideoCompressionPreset.entries
var localSelectedPreset by remember { mutableStateOf(selectedPreset) }
ListDialog(
title = stringResource(CommonStrings.dialog_video_quality_selector_title),
subtitle = stringResource(CommonStrings.dialog_default_video_quality_selector_subtitle),
onSubmit = { onSubmit(localSelectedPreset) },
onDismissRequest = onDismiss,
applyPaddingToContents = false,
) {
for (preset in videoPresets) {
val isSelected = preset == localSelectedPreset
item(
key = preset,
contentType = preset,
) {
val title = when (preset) {
VideoCompressionPreset.LOW -> stringResource(R.string.screen_advanced_settings_optimise_video_upload_quality_low)
VideoCompressionPreset.STANDARD -> stringResource(R.string.screen_advanced_settings_optimise_video_upload_quality_standard)
VideoCompressionPreset.HIGH -> stringResource(R.string.screen_advanced_settings_optimise_video_upload_quality_high)
}
val subtitle = when (preset) {
VideoCompressionPreset.LOW -> stringResource(CommonStrings.common_video_quality_low_description)
VideoCompressionPreset.STANDARD -> stringResource(CommonStrings.common_video_quality_standard_description)
VideoCompressionPreset.HIGH -> stringResource(CommonStrings.common_video_quality_high_description)
}
ListItem(
headlineContent = {
Text(
text = title,
style = ElementTheme.typography.fontBodyLgMedium,
)
},
supportingContent = {
Text(
text = subtitle,
style = ElementTheme.materialTypography.bodyMedium,
color = ElementTheme.colors.textSecondary,
)
},
leadingContent = ListItemContent.RadioButton(
selected = isSelected,
),
onClick = {
localSelectedPreset = preset
},
)
}
}
}
}
@Composable
private fun ModerationAndSafety(
state: AdvancedSettingsState,
modifier: Modifier = Modifier,
) {
PreferenceCategory(
modifier = modifier,
title = stringResource(R.string.screen_advanced_settings_moderation_and_safety_section_title),
showTopDivider = true
) {
PreferenceSwitch(
title = stringResource(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title),
isChecked = state.mediaPreviewConfigState.hideInviteAvatars,
onCheckedChange = {
state.eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(it))
},
enabled = !state.mediaPreviewConfigState.setHideInviteAvatarsAction.isLoading()
)
ListSectionHeader(
title = stringResource(R.string.screen_advanced_settings_show_media_timeline_title),
hasDivider = false,
description = {
ListSupportingText(
text = stringResource(R.string.screen_advanced_settings_show_media_timeline_subtitle),
contentPadding = ListSupportingTextDefaults.Padding.None,
)
}
)
ListItem(
headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_always_hide)) },
leadingContent = ListItemContent.RadioButton(
selected = state.mediaPreviewConfigState.timelineMediaPreviewValue == MediaPreviewValue.Off,
compact = true
),
onClick = {
state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off))
},
enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading()
)
ListItem(
headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_private_rooms)) },
leadingContent = ListItemContent.RadioButton(
selected = state.mediaPreviewConfigState.timelineMediaPreviewValue == MediaPreviewValue.Private,
compact = true
),
onClick = {
state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private))
},
enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading()
)
ListItem(
headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_always_show)) },
leadingContent = ListItemContent.RadioButton(
selected = state.mediaPreviewConfigState.timelineMediaPreviewValue == MediaPreviewValue.On,
compact = true
),
onClick = {
state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On))
},
enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading()
)
}
}
@PreviewWithLargeHeight
@Composable
internal fun AdvancedSettingsViewLightPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
ElementPreviewLight { ContentToPreview(state) }
@PreviewWithLargeHeight
@Composable
internal fun AdvancedSettingsViewDarkPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
ElementPreviewDark { ContentToPreview(state) }
@ExcludeFromCoverage
@Composable
private fun ContentToPreview(state: AdvancedSettingsState) {
AdvancedSettingsView(
state = state,
onBackClick = { }
)
}
@Composable
@PreviewsDayNight
internal fun VideoQualitySelectorDialogPreview() {
ElementPreview {
VideoQualitySelectorDialog(
selectedPreset = VideoCompressionPreset.STANDARD,
onSubmit = { /* no-op */ },
onDismiss = { /* no-op */ }
)
}
}
@@ -0,0 +1,122 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
data class MediaPreviewConfigState(
val hideInviteAvatars: Boolean,
val timelineMediaPreviewValue: MediaPreviewValue,
val setHideInviteAvatarsAction: AsyncAction<Unit>,
val setTimelineMediaPreviewAction: AsyncAction<Unit>,
)
interface MediaPreviewConfigStateStore {
@Composable
fun state(): MediaPreviewConfigState
fun setHideInviteAvatars(hide: Boolean)
fun setTimelineMediaPreviewValue(value: MediaPreviewValue)
}
@ContributesBinding(SessionScope::class)
@SingleIn(SessionScope::class)
class DefaultMediaPreviewConfigStateStore(
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val mediaPreviewService: MediaPreviewService,
private val snackbarDispatcher: SnackbarDispatcher,
) : MediaPreviewConfigStateStore {
private val hideInviteAvatars = mutableStateOf(false)
private val timelineMediaPreviewValue = mutableStateOf(MediaPreviewValue.On)
private val setHideInviteAvatarsAction = mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
private val setTimelineMediaPreviewAction = mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
init {
val configFlow = mediaPreviewService.mediaPreviewConfigFlow
val hideInviteAvatarsFlow = configFlow.map { it.hideInviteAvatar }.distinctUntilChanged()
val timelineMediaPreviewFlow = configFlow.map { it.mediaPreviewValue }.distinctUntilChanged()
hideInviteAvatarsFlow
.onEach {
Timber.d("Hide invite avatars changed to $it")
hideInviteAvatars.value = it
}
.launchIn(sessionCoroutineScope)
timelineMediaPreviewFlow
.onEach {
Timber.d("Timeline media preview value changed to $it")
timelineMediaPreviewValue.value = it
}
.launchIn(sessionCoroutineScope)
}
@Composable
override fun state(): MediaPreviewConfigState {
return MediaPreviewConfigState(
hideInviteAvatars = hideInviteAvatars.value,
timelineMediaPreviewValue = timelineMediaPreviewValue.value,
setHideInviteAvatarsAction = setHideInviteAvatarsAction.value,
setTimelineMediaPreviewAction = setTimelineMediaPreviewAction.value,
)
}
override fun setHideInviteAvatars(hide: Boolean) {
sessionCoroutineScope.launch {
val prevHideInviteAvatars = hideInviteAvatars.value
if (prevHideInviteAvatars == hide) return@launch
Timber.d("Setting hide invite avatars to $hide")
hideInviteAvatars.value = hide
runUpdatingState(setHideInviteAvatarsAction) {
mediaPreviewService
.setHideInviteAvatars(hide)
.onFailure {
hideInviteAvatars.value = prevHideInviteAvatars
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_something_went_wrong_message))
}
}
}
}
override fun setTimelineMediaPreviewValue(value: MediaPreviewValue) {
sessionCoroutineScope.launch {
val prevTimelineMediaPreviewValue = timelineMediaPreviewValue.value
if (prevTimelineMediaPreviewValue == value) return@launch
Timber.d("Setting timeline media preview value to $value")
timelineMediaPreviewValue.value = value
runUpdatingState(setTimelineMediaPreviewAction) {
mediaPreviewService
.setMediaPreviewValue(value)
.onFailure {
timelineMediaPreviewValue.value = prevTimelineMediaPreviewValue
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_something_went_wrong_message))
}
}
}
}
}
@@ -0,0 +1,37 @@
/*
* 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.preferences.impl.analytics
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class AnalyticsSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: AnalyticsSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
AnalyticsSettingsView(
state = state,
onBackClick = ::navigateUp,
modifier = modifier
)
}
}
@@ -0,0 +1,28 @@
/*
* 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.preferences.impl.analytics
import androidx.compose.runtime.Composable
import dev.zacsweers.metro.Inject
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
import io.element.android.libraries.architecture.Presenter
@Inject
class AnalyticsSettingsPresenter(
private val analyticsPreferencesPresenter: Presenter<AnalyticsPreferencesState>,
) : Presenter<AnalyticsSettingsState> {
@Composable
override fun present(): AnalyticsSettingsState {
val analyticsPreferencesState = analyticsPreferencesPresenter.present()
return AnalyticsSettingsState(
analyticsPreferencesState = analyticsPreferencesState,
)
}
}
@@ -0,0 +1,15 @@
/*
* 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.preferences.impl.analytics
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
data class AnalyticsSettingsState(
val analyticsPreferencesState: AnalyticsPreferencesState,
)
@@ -0,0 +1,23 @@
/*
* 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.preferences.impl.analytics
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.analytics.api.preferences.aAnalyticsPreferencesState
open class AnalyticsSettingsStateProvider : PreviewParameterProvider<AnalyticsSettingsState> {
override val values: Sequence<AnalyticsSettingsState>
get() = sequenceOf(
aAnalyticsSettingsState(),
)
}
fun aAnalyticsSettingsState() = AnalyticsSettingsState(
analyticsPreferencesState = aAnalyticsPreferencesState(),
)
@@ -0,0 +1,45 @@
/*
* 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.preferences.impl.analytics
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesView
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
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
fun AnalyticsSettingsView(
state: AnalyticsSettingsState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferencePage(
modifier = modifier,
onBackClick = onBackClick,
title = stringResource(id = CommonStrings.common_analytics)
) {
AnalyticsPreferencesView(
state = state.analyticsPreferencesState,
)
}
}
@PreviewsDayNight
@Composable
internal fun AnalyticsSettingsViewPreview(@PreviewParameter(AnalyticsSettingsStateProvider::class) state: AnalyticsSettingsState) = ElementPreview {
AnalyticsSettingsView(
state = state,
onBackClick = {},
)
}
@@ -0,0 +1,17 @@
/*
* 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.preferences.impl.blockedusers
import io.element.android.libraries.matrix.api.core.UserId
sealed interface BlockedUsersEvents {
data class Unblock(val userId: UserId) : BlockedUsersEvents
data object ConfirmUnblock : BlockedUsersEvents
data object Cancel : BlockedUsersEvents
}
@@ -0,0 +1,37 @@
/*
* 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.preferences.impl.blockedusers
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class BlockedUsersNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: BlockedUsersPresenter,
) : Node(buildContext = buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
BlockedUsersView(
state = state,
onBackClick = ::navigateUp,
modifier = modifier,
)
}
}
@@ -0,0 +1,98 @@
/*
* 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.preferences.impl.blockedusers
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
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.setValue
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Inject
class BlockedUsersPresenter(
private val matrixClient: MatrixClient,
private val featureFlagService: FeatureFlagService,
) : Presenter<BlockedUsersState> {
@Composable
override fun present(): BlockedUsersState {
val coroutineScope = rememberCoroutineScope()
var pendingUserToUnblock by remember {
mutableStateOf<UserId?>(null)
}
val unblockUserAction: MutableState<AsyncAction<Unit>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
val renderBlockedUsersDetail by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.ShowBlockedUsersDetails)
}.collectAsState(initial = false)
val ignoredUserIds by matrixClient.ignoredUsersFlow.collectAsState()
val ignoredMatrixUser by produceState(
initialValue = ignoredUserIds.map { MatrixUser(userId = it) },
key1 = renderBlockedUsersDetail,
key2 = ignoredUserIds
) {
value = ignoredUserIds.map {
if (renderBlockedUsersDetail) {
matrixClient.getProfile(it).getOrNull()
} else {
null
}
?: MatrixUser(userId = it)
}
}
fun handleEvent(event: BlockedUsersEvents) {
when (event) {
is BlockedUsersEvents.Unblock -> {
pendingUserToUnblock = event.userId
unblockUserAction.value = AsyncAction.ConfirmingNoParams
}
BlockedUsersEvents.ConfirmUnblock -> {
pendingUserToUnblock?.let {
coroutineScope.unblockUser(it, unblockUserAction)
pendingUserToUnblock = null
}
}
BlockedUsersEvents.Cancel -> {
pendingUserToUnblock = null
unblockUserAction.value = AsyncAction.Uninitialized
}
}
}
return BlockedUsersState(
blockedUsers = ignoredMatrixUser.toImmutableList(),
unblockUserAction = unblockUserAction.value,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.unblockUser(userId: UserId, asyncAction: MutableState<AsyncAction<Unit>>) = launch {
runUpdatingState(asyncAction) {
matrixClient.unignoreUser(userId)
}
}
}
@@ -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.preferences.impl.blockedusers
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class BlockedUsersState(
val blockedUsers: ImmutableList<MatrixUser>,
val unblockUserAction: AsyncAction<Unit>,
val eventSink: (BlockedUsersEvents) -> Unit,
)
@@ -0,0 +1,40 @@
/*
* 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.preferences.impl.blockedusers
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import kotlinx.collections.immutable.toImmutableList
class BlockedUsersStateProvider : PreviewParameterProvider<BlockedUsersState> {
override val values: Sequence<BlockedUsersState>
get() = sequenceOf(
aBlockedUsersState(),
aBlockedUsersState(blockedUsers = aMatrixUserList().map { it.copy(displayName = null, avatarUrl = null) }),
aBlockedUsersState(blockedUsers = emptyList()),
aBlockedUsersState(unblockUserAction = AsyncAction.ConfirmingNoParams),
aBlockedUsersState(unblockUserAction = AsyncAction.Loading),
aBlockedUsersState(unblockUserAction = AsyncAction.Failure(RuntimeException("Failed to unblock user"))),
aBlockedUsersState(unblockUserAction = AsyncAction.Success(Unit)),
)
}
internal fun aBlockedUsersState(
blockedUsers: List<MatrixUser> = aMatrixUserList(),
unblockUserAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (BlockedUsersEvents) -> Unit = {},
): BlockedUsersState {
return BlockedUsersState(
blockedUsers = blockedUsers.toImmutableList(),
unblockUserAction = unblockUserAction,
eventSink = eventSink,
)
}
@@ -0,0 +1,126 @@
/*
* 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.preferences.impl.blockedusers
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
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.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BlockedUsersView(
state: BlockedUsersState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
Scaffold(
topBar = {
TopAppBar(
titleStr = stringResource(CommonStrings.common_blocked_users),
navigationIcon = {
BackButton(onClick = onBackClick)
}
)
}
) { padding ->
LazyColumn(
modifier = Modifier.padding(padding)
) {
items(state.blockedUsers) { matrixUser ->
BlockedUserItem(
matrixUser = matrixUser,
onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) }
)
}
}
}
val asyncIndicatorState = rememberAsyncIndicatorState()
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState)
when (state.unblockUserAction) {
is AsyncAction.Loading -> {
LaunchedEffect(state.unblockUserAction) {
asyncIndicatorState.enqueue {
AsyncIndicator.Loading(text = stringResource(R.string.screen_blocked_users_unblocking))
}
}
}
is AsyncAction.Failure -> {
LaunchedEffect(state.unblockUserAction) {
asyncIndicatorState.enqueue(durationMs = AsyncIndicator.DURATION_SHORT) {
AsyncIndicator.Failure(text = stringResource(CommonStrings.common_failed))
}
}
}
is AsyncAction.Success -> {
LaunchedEffect(state.unblockUserAction) {
asyncIndicatorState.clear()
}
}
is AsyncAction.Confirming -> {
ConfirmationDialog(
title = stringResource(R.string.screen_blocked_users_unblock_alert_title),
content = stringResource(R.string.screen_blocked_users_unblock_alert_description),
submitText = stringResource(R.string.screen_blocked_users_unblock_alert_action),
onSubmitClick = { state.eventSink(BlockedUsersEvents.ConfirmUnblock) },
onDismiss = { state.eventSink(BlockedUsersEvents.Cancel) }
)
}
else -> Unit
}
}
}
@Composable
private fun BlockedUserItem(
matrixUser: MatrixUser,
onClick: (UserId) -> Unit,
) {
MatrixUserRow(
modifier = Modifier.clickable { onClick(matrixUser.userId) },
matrixUser = matrixUser,
)
}
@PreviewsDayNight
@Composable
internal fun BlockedUsersViewPreview(@PreviewParameter(BlockedUsersStateProvider::class) state: BlockedUsersState) {
ElementPreview {
BlockedUsersView(
state = state,
onBackClick = {}
)
}
}
@@ -0,0 +1,24 @@
/*
* 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.preferences.impl.developer
import androidx.compose.ui.graphics.Color
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
sealed interface DeveloperSettingsEvents {
data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents
data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents
data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : DeveloperSettingsEvents
data class SetShowColorPicker(val show: Boolean) : DeveloperSettingsEvents
data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents
data object ClearCache : DeveloperSettingsEvents
}
@@ -0,0 +1,56 @@
/*
* 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.preferences.impl.developer
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.airbnb.android.showkase.models.Showkase
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.designsystem.showkase.getBrowserIntent
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class DeveloperSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: DeveloperSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToPushHistory()
fun onDone()
}
private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val activity = requireNotNull(LocalActivity.current)
fun openShowkase() {
val intent = Showkase.getBrowserIntent(activity)
activity.startActivity(intent)
}
val state = presenter.present()
DeveloperSettingsView(
state = state,
modifier = modifier,
onOpenShowkase = ::openShowkase,
onPushHistoryClick = callback::navigateToPushHistory,
onBackClick = callback::onDone,
)
}
}
@@ -0,0 +1,226 @@
/*
* 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.preferences.impl.developer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.graphics.toArgb
import dev.zacsweers.metro.Inject
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.preferences.impl.developer.tracing.toLogLevel
import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem
import io.element.android.features.preferences.impl.model.EnabledFeature
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.net.URL
@Inject
class DeveloperSettingsPresenter(
private val sessionId: SessionId,
private val featureFlagService: FeatureFlagService,
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
private val clearCacheUseCase: ClearCacheUseCase,
private val rageshakePresenter: Presenter<RageshakePreferencesState>,
private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta,
private val enterpriseService: EnterpriseService,
) : Presenter<DeveloperSettingsState> {
@Composable
override fun present(): DeveloperSettingsState {
val rageshakeState = rageshakePresenter.present()
val enabledFeatures = remember {
mutableStateListOf<EnabledFeature>()
}
val cacheSize = remember {
mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized)
}
val clearCacheAction = remember {
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
}
var showColorPicker by remember {
mutableStateOf(false)
}
val customElementCallBaseUrl by remember {
appPreferencesStore
.getCustomElementCallBaseUrlFlow()
}.collectAsState(initial = null)
val tracingLogLevelFlow = remember {
appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) }
}
val tracingLogLevel by tracingLogLevelFlow.collectAsState(initial = AsyncData.Uninitialized)
val tracingLogPacks by produceState(persistentListOf()) {
appPreferencesStore.getTracingLogPacksFlow()
// Sort the entries alphabetically by its title
.map { it.sortedBy { pack -> pack.title } }
.collectLatest { value = it.toImmutableList() }
}
LaunchedEffect(Unit) {
featureFlagService.getAvailableFeatures()
.run {
// Never display room directory search in release builds for Play Store
if (buildMeta.flavorDescription == "GooglePlay" && buildMeta.buildType == BuildType.RELEASE) {
filterNot { it.key == FeatureFlags.RoomDirectorySearch.key }
} else {
this
}
}
.forEach { feature ->
enabledFeatures.add(EnabledFeature(feature, featureFlagService.isFeatureEnabled(feature)))
}
}
val featureUiModels = createUiModels(enabledFeatures)
val coroutineScope = rememberCoroutineScope()
// Compute cache size each time the clear cache action value is changed
LaunchedEffect(clearCacheAction.value.isSuccess()) {
computeCacheSize(cacheSize)
}
fun handleEvent(event: DeveloperSettingsEvents) {
when (event) {
is DeveloperSettingsEvents.UpdateEnabledFeature -> coroutineScope.updateEnabledFeature(
enabledFeatures = enabledFeatures,
featureKey = event.feature.key,
enabled = event.isEnabled,
triggerClearCache = { handleEvent(DeveloperSettingsEvents.ClearCache) }
)
is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch {
val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() }
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
}
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
is DeveloperSettingsEvents.SetTracingLogLevel -> coroutineScope.launch {
appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel())
}
is DeveloperSettingsEvents.ToggleTracingLogPack -> coroutineScope.launch {
val currentPacks = tracingLogPacks.toMutableSet()
if (currentPacks.contains(event.logPack)) {
currentPacks.remove(event.logPack)
} else {
currentPacks.add(event.logPack)
}
appPreferencesStore.setTracingLogPacks(currentPacks)
}
is DeveloperSettingsEvents.ChangeBrandColor -> coroutineScope.launch {
showColorPicker = false
val color = event.color
?.toArgb()
?.toHexString(HexFormat.UpperCase)
?.substring(2, 8)
?.padStart(7, '#')
enterpriseService.overrideBrandColor(sessionId, color)
}
is DeveloperSettingsEvents.SetShowColorPicker -> {
showColorPicker = event.show
}
}
}
return DeveloperSettingsState(
features = featureUiModels,
cacheSize = cacheSize.value,
clearCacheAction = clearCacheAction.value,
rageshakeState = rageshakeState,
customElementCallBaseUrlState = CustomElementCallBaseUrlState(
baseUrl = customElementCallBaseUrl,
validator = ::customElementCallUrlValidator,
),
tracingLogLevel = tracingLogLevel,
tracingLogPacks = tracingLogPacks,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
showColorPicker = showColorPicker,
eventSink = ::handleEvent,
)
}
@Composable
private fun createUiModels(
enabledFeatures: SnapshotStateList<EnabledFeature>,
): ImmutableList<FeatureUiModel> {
return enabledFeatures.map { enabledFeature ->
key(enabledFeature.feature.key) {
remember(enabledFeature) {
FeatureUiModel(
key = enabledFeature.feature.key,
title = enabledFeature.feature.title,
description = enabledFeature.feature.description,
icon = null,
isEnabled = enabledFeature.isEnabled
)
}
}
}.toImmutableList()
}
private fun CoroutineScope.updateEnabledFeature(
enabledFeatures: SnapshotStateList<EnabledFeature>,
featureKey: String,
enabled: Boolean,
@Suppress("UNUSED_PARAMETER") triggerClearCache: () -> Unit,
) = launch {
val featureIndex = enabledFeatures.indexOfFirst { it.feature.key == featureKey }.takeIf { it != -1 } ?: return@launch
val feature = enabledFeatures[featureIndex].feature
if (featureFlagService.setFeatureEnabled(feature, enabled)) {
enabledFeatures[featureIndex] = enabledFeatures[featureIndex].copy(isEnabled = enabled)
}
}
private fun CoroutineScope.computeCacheSize(cacheSize: MutableState<AsyncData<String>>) = launch {
suspend {
computeCacheSizeUseCase()
}.runCatchingUpdatingState(cacheSize)
}
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncAction<Unit>>) = launch {
suspend {
clearCacheUseCase()
}.runCatchingUpdatingState(clearCacheAction)
}
}
private fun customElementCallUrlValidator(url: String?): Boolean {
return runCatchingExceptions {
if (url.isNullOrEmpty()) return@runCatchingExceptions
val parsedUrl = URL(url)
if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol")
if (parsedUrl.host.isNullOrBlank()) error("Missing host")
}.isSuccess
}
@@ -0,0 +1,37 @@
/*
* 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.preferences.impl.developer
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
import kotlinx.collections.immutable.ImmutableList
data class DeveloperSettingsState(
val features: ImmutableList<FeatureUiModel>,
val cacheSize: AsyncData<String>,
val rageshakeState: RageshakePreferencesState,
val clearCacheAction: AsyncAction<Unit>,
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
val tracingLogLevel: AsyncData<LogLevelItem>,
val tracingLogPacks: ImmutableList<TraceLogPack>,
val isEnterpriseBuild: Boolean,
val showColorPicker: Boolean,
val eventSink: (DeveloperSettingsEvents) -> Unit
) {
val showLoader = clearCacheAction is AsyncAction.Loading
}
data class CustomElementCallBaseUrlState(
val baseUrl: String?,
val validator: (String?) -> Boolean,
)
@@ -0,0 +1,65 @@
/*
* 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.preferences.impl.developer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
import kotlinx.collections.immutable.toImmutableList
open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSettingsState> {
override val values: Sequence<DeveloperSettingsState>
get() = sequenceOf(
aDeveloperSettingsState(),
aDeveloperSettingsState(
clearCacheAction = AsyncAction.Loading
),
aDeveloperSettingsState(
customElementCallBaseUrlState = aCustomElementCallBaseUrlState(
baseUrl = "https://call.element.ahoy",
)
),
aDeveloperSettingsState(
isEnterpriseBuild = true,
showColorPicker = true,
),
)
}
fun aDeveloperSettingsState(
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
traceLogPacks: List<TraceLogPack> = emptyList(),
isEnterpriseBuild: Boolean = false,
showColorPicker: Boolean = false,
eventSink: (DeveloperSettingsEvents) -> Unit = {},
) = DeveloperSettingsState(
features = aFeatureUiModelList(),
rageshakeState = aRageshakePreferencesState(),
cacheSize = AsyncData.Success("1.2 MB"),
clearCacheAction = clearCacheAction,
customElementCallBaseUrlState = customElementCallBaseUrlState,
tracingLogLevel = AsyncData.Success(LogLevelItem.INFO),
tracingLogPacks = traceLogPacks.toImmutableList(),
isEnterpriseBuild = isEnterpriseBuild,
showColorPicker = showColorPicker,
eventSink = eventSink,
)
fun aCustomElementCallBaseUrlState(
baseUrl: String? = null,
validator: (String?) -> Boolean = { true },
) = CustomElementCallBaseUrlState(
baseUrl = baseUrl,
validator = validator,
)
@@ -0,0 +1,250 @@
/*
* 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.preferences.impl.developer
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.featureflag.ui.FeatureListView
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
import io.element.android.libraries.ui.strings.CommonStrings
import io.mhssn.colorpicker.ColorPickerDialog
import io.mhssn.colorpicker.ColorPickerType
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DeveloperSettingsView(
state: DeveloperSettingsState,
onOpenShowkase: () -> Unit,
onPushHistoryClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state.showLoader) {
ProgressDialog()
}
BackHandler(
enabled = !state.showLoader,
onBack = onBackClick,
)
PreferencePage(
modifier = modifier,
onBackClick = {
if (!state.showLoader) {
onBackClick()
}
},
title = stringResource(id = CommonStrings.common_developer_options)
) {
// Note: this is OK to hardcode strings in this debug screen.
PreferenceCategory(
title = "Feature flags",
) {
FeatureListContent(state)
}
NotificationCategory(onPushHistoryClick)
ElementCallCategory(state = state)
PreferenceCategory(title = "Rust SDK") {
PreferenceDropdown(
title = "Tracing log level",
supportingText = "Requires app reboot",
selectedOption = state.tracingLogLevel.dataOrNull(),
options = LogLevelItem.entries.toImmutableList(),
onSelectOption = { logLevel ->
state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(logLevel))
}
)
}
PreferenceCategory(title = "Enable trace logs per SDK feature") {
Text(
text = "Requires app reboot",
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
)
for (logPack in TraceLogPack.entries) {
PreferenceSwitch(
title = logPack.title,
isChecked = state.tracingLogPacks.contains(logPack),
onCheckedChange = { isChecked -> state.eventSink(DeveloperSettingsEvents.ToggleTracingLogPack(logPack, isChecked)) }
)
}
}
PreferenceCategory(title = "Showkase") {
ListItem(
headlineContent = {
Text("Open Showkase browser")
},
onClick = onOpenShowkase
)
}
RageshakePreferencesView(
state = state.rageshakeState,
)
if (state.isEnterpriseBuild) {
PreferenceCategory(title = "Theme") {
ListItem(
headlineContent = {
Text("Change brand color")
},
onClick = {
state.eventSink(DeveloperSettingsEvents.SetShowColorPicker(true))
}
)
ListItem(
headlineContent = {
Text("Reset brand color")
},
onClick = {
state.eventSink(DeveloperSettingsEvents.ChangeBrandColor(null))
}
)
}
}
PreferenceCategory(title = "Crash") {
ListItem(
headlineContent = {
Text("Crash the app 💥")
},
onClick = { error("This crash is a test.") }
)
}
val cache = state.cacheSize
PreferenceCategory(title = "Cache") {
ListItem(
headlineContent = {
Text("Clear cache")
},
trailingContent = if (state.cacheSize.isLoading() || state.clearCacheAction.isLoading()) {
ListItemContent.Custom {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(20.dp),
strokeWidth = 2.dp
)
}
} else {
ListItemContent.Text(cache.dataOrNull().orEmpty())
},
onClick = {
if (state.clearCacheAction.isLoading().not()) {
state.eventSink(DeveloperSettingsEvents.ClearCache)
}
}
)
}
}
ColorPickerDialog(
show = state.showColorPicker,
type = ColorPickerType.Classic(
showAlphaBar = false,
),
onDismissRequest = {
state.eventSink(DeveloperSettingsEvents.SetShowColorPicker(false))
},
onPickedColor = {
state.eventSink(DeveloperSettingsEvents.ChangeBrandColor(it))
},
)
}
@Composable
private fun ElementCallCategory(
state: DeveloperSettingsState,
) {
PreferenceCategory(title = "Element Call") {
val callUrlState = state.customElementCallBaseUrlState
val supportingText = if (callUrlState.baseUrl.isNullOrEmpty()) {
stringResource(R.string.screen_advanced_settings_element_call_base_url_description)
} else {
callUrlState.baseUrl
}
PreferenceTextField(
headline = stringResource(R.string.screen_advanced_settings_element_call_base_url),
value = callUrlState.baseUrl,
placeholder = "https://.../room",
supportingText = supportingText,
validation = callUrlState.validator,
onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error),
displayValue = { value -> !value.isNullOrEmpty() },
keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri),
onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) }
)
}
}
@Composable
private fun NotificationCategory(onPushHistoryClick: () -> Unit) {
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_title)) {
ListItem(
headlineContent = {
Text(stringResource(R.string.troubleshoot_notifications_entry_point_push_history_title))
},
onClick = onPushHistoryClick,
)
}
}
@Composable
private fun FeatureListContent(
state: DeveloperSettingsState,
) {
fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) {
state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, isEnabled))
}
FeatureListView(
features = state.features,
onCheckedChange = ::onFeatureEnabled,
)
}
@PreviewsDayNight
@Composable
internal fun DeveloperSettingsViewPreview(
@PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState
) = ElementPreview {
DeveloperSettingsView(
state = state,
onOpenShowkase = {},
onPushHistoryClick = {},
onBackClick = {}
)
}
@@ -0,0 +1,35 @@
/*
* 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.preferences.impl.developer.tracing
import androidx.compose.runtime.Composable
import io.element.android.libraries.designsystem.components.preferences.DropdownOption
enum class LogLevelItem : DropdownOption {
ERROR {
@Composable
override fun getText(): String = "Error"
},
WARN {
@Composable
override fun getText(): String = "Warn"
},
INFO {
@Composable
override fun getText(): String = "Info"
},
DEBUG {
@Composable
override fun getText(): String = "Debug"
},
TRACE {
@Composable
override fun getText(): String = "Trace"
}
}
@@ -0,0 +1,31 @@
/*
* 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.preferences.impl.developer.tracing
import io.element.android.libraries.matrix.api.tracing.LogLevel
fun LogLevelItem.toLogLevel(): LogLevel {
return when (this) {
LogLevelItem.ERROR -> io.element.android.libraries.matrix.api.tracing.LogLevel.ERROR
LogLevelItem.WARN -> io.element.android.libraries.matrix.api.tracing.LogLevel.WARN
LogLevelItem.INFO -> io.element.android.libraries.matrix.api.tracing.LogLevel.INFO
LogLevelItem.DEBUG -> io.element.android.libraries.matrix.api.tracing.LogLevel.DEBUG
LogLevelItem.TRACE -> io.element.android.libraries.matrix.api.tracing.LogLevel.TRACE
}
}
fun LogLevel.toLogLevelItem(): LogLevelItem {
return when (this) {
LogLevel.ERROR -> LogLevelItem.ERROR
LogLevel.WARN -> LogLevelItem.WARN
LogLevel.INFO -> LogLevelItem.INFO
LogLevel.DEBUG -> LogLevelItem.DEBUG
LogLevel.TRACE -> LogLevelItem.TRACE
}
}
@@ -0,0 +1,15 @@
/*
* 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.preferences.impl.labs
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
sealed interface LabsEvents {
data class ToggleFeature(val feature: FeatureUiModel) : LabsEvents
}
@@ -0,0 +1,43 @@
/*
* 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.preferences.impl.labs
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class LabsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: LabsPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onDone()
}
val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LabsView(
state = state,
onBack = callback::onDone,
)
}
}
@@ -0,0 +1,114 @@
/*
* 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.preferences.impl.labs
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import dev.zacsweers.metro.Inject
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.model.EnabledFeature
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Inject
class LabsPresenter(
private val stringProvider: StringProvider,
private val featureFlagService: FeatureFlagService,
private val clearCacheUseCase: ClearCacheUseCase,
) : Presenter<LabsState> {
@Composable
override fun present(): LabsState {
val coroutineScope = rememberCoroutineScope()
val enabledFeatures = remember {
mutableStateListOf<EnabledFeature>()
}
LaunchedEffect(Unit) {
featureFlagService.getAvailableFeatures(isInLabs = true)
.forEach { feature ->
enabledFeatures.add(EnabledFeature(feature, featureFlagService.isFeatureEnabled(feature)))
}
}
var isApplyingChanges by remember { mutableStateOf(false) }
val featureUiModels = createUiModels(enabledFeatures)
fun handleEvent(event: LabsEvents) {
when (event) {
is LabsEvents.ToggleFeature -> coroutineScope.launch {
val featureIndex = enabledFeatures.indexOfFirst { it.feature.key == event.feature.key }.takeIf { it != -1 } ?: return@launch
val enabledFeature = enabledFeatures[featureIndex]
val feature = enabledFeature.feature
val newValue = enabledFeature.isEnabled.not()
if (featureFlagService.setFeatureEnabled(feature, newValue)) {
enabledFeatures[featureIndex] = enabledFeatures[featureIndex].copy(isEnabled = newValue)
when (feature.key) {
FeatureFlags.Threads.key -> {
// Threads require a cache clear to recreate the event cache
clearCacheUseCase()
isApplyingChanges = true
}
}
}
}
}
}
return LabsState(
features = featureUiModels,
isApplyingChanges = isApplyingChanges,
eventSink = ::handleEvent,
)
}
@Composable
private fun createUiModels(
enabledFeatures: SnapshotStateList<EnabledFeature>,
): ImmutableList<FeatureUiModel> {
return enabledFeatures.map { enabledFeature ->
key(enabledFeature.feature.key) {
val title = when (enabledFeature.feature) {
FeatureFlags.Threads -> stringProvider.getString(R.string.screen_labs_enable_threads)
else -> enabledFeature.feature.title
}
val description = when (enabledFeature.feature) {
FeatureFlags.Threads -> stringProvider.getString(R.string.screen_labs_enable_threads_description)
else -> enabledFeature.feature.description
}
val icon = when (enabledFeature.feature) {
FeatureFlags.Threads -> CompoundIcons.Threads()
else -> null
}
remember(enabledFeature) {
FeatureUiModel(
key = enabledFeature.feature.key,
title = title,
description = description,
icon = icon?.let(IconSource::Vector),
isEnabled = enabledFeature.isEnabled
)
}
}
}.toImmutableList()
}
}
@@ -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.preferences.impl.labs
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.ImmutableList
data class LabsState(
val features: ImmutableList<FeatureUiModel>,
val isApplyingChanges: Boolean,
val eventSink: (LabsEvents) -> Unit,
)
@@ -0,0 +1,49 @@
/*
* 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.preferences.impl.labs
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.toImmutableList
internal class LabsStateProvider : PreviewParameterProvider<LabsState> {
override val values: Sequence<LabsState>
get() = sequenceOf(
aLabsState(features = aFeatureList()),
aLabsState(features = aFeatureList(), isApplyingChanges = true),
)
}
internal fun aLabsState(
features: List<FeatureUiModel> = emptyList(),
isApplyingChanges: Boolean = false,
) = LabsState(
features = features.toImmutableList(),
isApplyingChanges = isApplyingChanges,
eventSink = {},
)
internal fun aFeatureList() = listOf(
FeatureUiModel(
key = "feature_1",
title = "Feature 1",
description = "This is a description of feature 1.",
isEnabled = true,
icon = IconSource.Resource(CompoundDrawables.ic_compound_threads),
),
FeatureUiModel(
key = "feature_2",
title = "Feature 2",
description = "This is a description of feature 2.",
isEnabled = false,
icon = IconSource.Resource(CompoundDrawables.ic_compound_video_call),
)
)
@@ -0,0 +1,108 @@
/*
* 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.preferences.impl.labs
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.preferences.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.list.SwitchListItem
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.TopAppBar
/**
* The contents of the Labs screen.
* Design: https://www.figma.com/design/V0dkfRAW6T3yCQKjahpzkX/ER-46-EX--Threads?node-id=2004-27319&t=yssy1yYYigsGON3s-0
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LabsView(
state: LabsState,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state.isApplyingChanges) {
ProgressDialog()
}
BackHandler(
enabled = !state.isApplyingChanges,
onBack = onBack,
)
HeaderFooterPage(
modifier = modifier
.fillMaxSize()
.systemBarsPadding()
.imePadding(),
topBar = {
TopAppBar(
titleStr = stringResource(R.string.screen_labs_title),
navigationIcon = {
BackButton(onClick = onBack, enabled = !state.isApplyingChanges)
}
)
},
header = {
IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp),
title = stringResource(R.string.screen_labs_header_title),
subTitle = stringResource(R.string.screen_labs_header_description),
iconStyle = BigIcon.Style.Default(CompoundIcons.Labs())
)
},
contentPadding = PaddingValues(),
content = {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 20.dp),
) {
items(items = state.features, key = { it.key }) { feature ->
SwitchListItem(
leadingContent = feature.icon?.let { ListItemContent.Icon(it) },
headline = feature.title,
supportingText = feature.description,
value = feature.isEnabled,
onChange = {
state.eventSink(LabsEvents.ToggleFeature(feature))
}
)
}
}
}
)
}
@PreviewsDayNight
@Composable
internal fun LabsViewPreview(@PreviewParameter(LabsStateProvider::class) state: LabsState) {
ElementPreview {
LabsView(state = state, onBack = {})
}
}
@@ -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.preferences.impl.model
import io.element.android.libraries.featureflag.api.Feature
data class EnabledFeature(
val feature: Feature,
val isEnabled: Boolean,
)
@@ -0,0 +1,23 @@
/*
* 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.preferences.impl.notifications
sealed interface NotificationSettingsEvents {
data object RefreshSystemNotificationsEnabled : NotificationSettingsEvents
data class SetNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data class SetAtRoomNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data class SetCallNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data class SetInviteForMeNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data object FixConfigurationMismatch : NotificationSettingsEvents
data object ClearConfigurationMismatchError : NotificationSettingsEvents
data object ClearNotificationChangeError : NotificationSettingsEvents
data object ChangePushProvider : NotificationSettingsEvents
data object CancelChangePushProvider : NotificationSettingsEvents
data class SetPushProvider(val index: Int) : NotificationSettingsEvents
}
@@ -0,0 +1,47 @@
/*
* 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.preferences.impl.notifications
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class NotificationSettingsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: NotificationSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToEditDefaultNotificationSetting(isOneToOne: Boolean)
fun navigateToTroubleshootNotifications()
}
private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
NotificationSettingsView(
state = state,
onOpenEditDefault = callback::navigateToEditDefaultNotificationSetting,
onBackClick = ::navigateUp,
onTroubleshootNotificationsClick = callback::navigateToTroubleshootNotifications,
modifier = modifier,
)
}
}
@@ -0,0 +1,274 @@
/*
* 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.preferences.impl.notifications
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
@Inject
class NotificationSettingsPresenter(
private val notificationSettingsService: NotificationSettingsService,
private val userPushStoreFactory: UserPushStoreFactory,
private val matrixClient: MatrixClient,
private val pushService: PushService,
private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider,
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
) : Presenter<NotificationSettingsState> {
@Composable
override fun present(): NotificationSettingsState {
val userPushStore = remember { userPushStoreFactory.getOrCreate(matrixClient.sessionId) }
val systemNotificationsEnabled: MutableState<Boolean> = remember {
mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled())
}
val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val localCoroutineScope = rememberCoroutineScope()
val appNotificationsEnabled by remember {
userPushStore.getNotificationEnabledForDevice()
}.collectAsState(initial = false)
val matrixSettings: MutableState<NotificationSettingsState.MatrixSettings> = remember {
mutableStateOf(NotificationSettingsState.MatrixSettings.Uninitialized)
}
// Used to force a recomposition
var refreshFullScreenIntentSettings by remember { mutableIntStateOf(0) }
LaunchedEffect(Unit) {
fetchSettings(matrixSettings)
observeNotificationSettings(matrixSettings, changeNotificationSettingAction)
}
// List of PushProvider -> Distributor
val distributors = remember {
pushService.getAvailablePushProviders()
.flatMap { pushProvider ->
pushProvider.getDistributors().map { distributor ->
pushProvider to distributor
}
}
}
// List of Distributors
val availableDistributors = remember {
distributors.map { it.second }.toImmutableList()
}
var currentDistributor by remember { mutableStateOf<AsyncData<Distributor>>(AsyncData.Uninitialized) }
var refreshPushProvider by remember { mutableIntStateOf(0) }
LaunchedEffect(refreshPushProvider) {
val p = pushService.getCurrentPushProvider(matrixClient.sessionId)
val distributor = p?.getCurrentDistributor(matrixClient.sessionId)
currentDistributor = if (distributor != null) {
AsyncData.Success(distributor)
} else {
AsyncData.Failure(Exception("Failed to get current push provider"))
}
}
var showChangePushProviderDialog by remember { mutableStateOf(false) }
fun CoroutineScope.changePushProvider(
data: Pair<PushProvider, Distributor>?
) = launch {
showChangePushProviderDialog = false
data ?: return@launch
val (pushProvider, distributor) = data
// No op if the distributor is the same.
if (distributor == currentDistributor.dataOrNull()) return@launch
currentDistributor = AsyncData.Loading(currentDistributor.dataOrNull())
pushService.registerWith(
matrixClient = matrixClient,
pushProvider = pushProvider,
distributor = distributor
)
.fold(
{
refreshPushProvider++
},
{
currentDistributor = AsyncData.Failure(it)
}
)
}
fun handleEvent(event: NotificationSettingsEvents) {
when (event) {
is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> {
localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled, changeNotificationSettingAction)
}
is NotificationSettingsEvents.SetCallNotificationsEnabled -> {
localCoroutineScope.setCallNotificationsEnabled(event.enabled, changeNotificationSettingAction)
}
is NotificationSettingsEvents.SetInviteForMeNotificationsEnabled -> {
localCoroutineScope.setInviteForMeNotificationsEnabled(event.enabled, changeNotificationSettingAction)
}
is NotificationSettingsEvents.SetNotificationsEnabled -> sessionCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled)
NotificationSettingsEvents.ClearConfigurationMismatchError -> {
matrixSettings.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)
}
NotificationSettingsEvents.FixConfigurationMismatch -> localCoroutineScope.fixConfigurationMismatch(matrixSettings)
NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> {
systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled()
refreshFullScreenIntentSettings++
}
NotificationSettingsEvents.ClearNotificationChangeError -> changeNotificationSettingAction.value = AsyncAction.Uninitialized
NotificationSettingsEvents.ChangePushProvider -> showChangePushProviderDialog = true
NotificationSettingsEvents.CancelChangePushProvider -> showChangePushProviderDialog = false
is NotificationSettingsEvents.SetPushProvider -> localCoroutineScope.changePushProvider(distributors.getOrNull(event.index))
}
}
return NotificationSettingsState(
matrixSettings = matrixSettings.value,
appSettings = NotificationSettingsState.AppSettings(
systemNotificationsEnabled = systemNotificationsEnabled.value,
appNotificationsEnabled = appNotificationsEnabled,
),
changeNotificationSettingAction = changeNotificationSettingAction.value,
currentPushDistributor = currentDistributor,
availablePushDistributors = availableDistributors,
showChangePushProviderDialog = showChangePushProviderDialog,
fullScreenIntentPermissionsState = key(refreshFullScreenIntentSettings) { fullScreenIntentPermissionsPresenter.present() },
eventSink = ::handleEvent,
)
}
@OptIn(FlowPreview::class)
private fun CoroutineScope.observeNotificationSettings(
target: MutableState<NotificationSettingsState.MatrixSettings>,
changeNotificationSettingAction: MutableState<AsyncAction<Unit>>,
) {
notificationSettingsService.notificationSettingsChangeFlow
.debounce(0.5.seconds)
.onEach {
fetchSettings(target)
changeNotificationSettingAction.value = AsyncAction.Uninitialized
}
.launchIn(this)
}
private fun CoroutineScope.fetchSettings(target: MutableState<NotificationSettingsState.MatrixSettings>) = launch {
val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow()
val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow()
val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow()
val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow()
if (groupDefaultMode != encryptedGroupDefaultMode || oneToOneDefaultMode != encryptedOneToOneDefaultMode) {
target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)
return@launch
}
val callNotificationsEnabled = notificationSettingsService.isCallEnabled().getOrThrow()
val atRoomNotificationsEnabled = notificationSettingsService.isRoomMentionEnabled().getOrThrow()
val inviteForMeNotificationsEnabled = notificationSettingsService.isInviteForMeEnabled().getOrThrow()
target.value = NotificationSettingsState.MatrixSettings.Valid(
atRoomNotificationsEnabled = atRoomNotificationsEnabled,
callNotificationsEnabled = callNotificationsEnabled,
inviteForMeNotificationsEnabled = inviteForMeNotificationsEnabled,
defaultGroupNotificationMode = encryptedGroupDefaultMode,
defaultOneToOneNotificationMode = encryptedOneToOneDefaultMode,
)
}
private fun CoroutineScope.fixConfigurationMismatch(target: MutableState<NotificationSettingsState.MatrixSettings>) = launch {
runCatchingExceptions {
val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow()
val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow()
if (groupDefaultMode != encryptedGroupDefaultMode) {
notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = encryptedGroupDefaultMode != RoomNotificationMode.ALL_MESSAGES,
mode = RoomNotificationMode.ALL_MESSAGES,
isOneToOne = false,
)
}
val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow()
val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow()
if (oneToOneDefaultMode != encryptedOneToOneDefaultMode) {
notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = encryptedOneToOneDefaultMode != RoomNotificationMode.ALL_MESSAGES,
mode = RoomNotificationMode.ALL_MESSAGES,
isOneToOne = true,
)
}
}.fold(
onSuccess = {},
onFailure = {
target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = true)
}
)
}
private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean, action: MutableState<AsyncAction<Unit>>) = launch {
action.runUpdatingStateNoSuccess {
notificationSettingsService.setRoomMentionEnabled(enabled)
}
}
private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean, action: MutableState<AsyncAction<Unit>>) = launch {
action.runUpdatingStateNoSuccess {
notificationSettingsService.setCallEnabled(enabled)
}
}
private fun CoroutineScope.setInviteForMeNotificationsEnabled(enabled: Boolean, action: MutableState<AsyncAction<Unit>>) = launch {
action.runUpdatingStateNoSuccess {
notificationSettingsService.setInviteForMeEnabled(enabled)
}
}
private fun CoroutineScope.setNotificationsEnabled(userPushStore: UserPushStore, enabled: Boolean) = launch {
userPushStore.setNotificationEnabledForDevice(enabled)
if (enabled) {
pushService.ensurePusherIsRegistered(matrixClient)
} else {
pushService.getCurrentPushProvider(matrixClient.sessionId)?.unregister(matrixClient)
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.preferences.impl.notifications
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.pushproviders.api.Distributor
import kotlinx.collections.immutable.ImmutableList
data class NotificationSettingsState(
val matrixSettings: MatrixSettings,
val appSettings: AppSettings,
val changeNotificationSettingAction: AsyncAction<Unit>,
val currentPushDistributor: AsyncData<Distributor>,
val availablePushDistributors: ImmutableList<Distributor>,
val showChangePushProviderDialog: Boolean,
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
val eventSink: (NotificationSettingsEvents) -> Unit,
) {
sealed interface MatrixSettings {
data object Uninitialized : MatrixSettings
data class Valid(
val atRoomNotificationsEnabled: Boolean,
val callNotificationsEnabled: Boolean,
val inviteForMeNotificationsEnabled: Boolean,
val defaultGroupNotificationMode: RoomNotificationMode?,
val defaultOneToOneNotificationMode: RoomNotificationMode?,
) : MatrixSettings
data class Invalid(
val fixFailed: Boolean
) : MatrixSettings
}
data class AppSettings(
val systemNotificationsEnabled: Boolean,
val appNotificationsEnabled: Boolean,
)
/**
* Whether the advanced settings should be shown.
* This is true if the current push distributor is in a failure state or if there are multiple push distributors available.
*/
val showAdvancedSettings: Boolean = currentPushDistributor.isFailure() || availablePushDistributors.size > 1
}
@@ -0,0 +1,111 @@
/*
* 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.preferences.impl.notifications
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.pushproviders.api.Distributor
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
open class NotificationSettingsStateProvider : PreviewParameterProvider<NotificationSettingsState> {
override val values: Sequence<NotificationSettingsState>
get() = sequenceOf(
aValidNotificationSettingsState(systemNotificationsEnabled = false),
aValidNotificationSettingsState(),
aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading),
aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(RuntimeException("error"))),
aValidNotificationSettingsState(
availablePushDistributors = listOf(aDistributor("Firebase")),
changeNotificationSettingAction = AsyncAction.Failure(RuntimeException("error")),
),
aValidNotificationSettingsState(availablePushDistributors = listOf(aDistributor("Firebase"))),
aValidNotificationSettingsState(showChangePushProviderDialog = true),
aValidNotificationSettingsState(
availablePushDistributors = listOf(
aDistributor("Firebase"),
aDistributor("ntfy", "app.id1"),
aDistributor("ntfy", "app.id2"),
),
showChangePushProviderDialog = true,
),
aValidNotificationSettingsState(currentPushDistributor = AsyncData.Loading()),
aValidNotificationSettingsState(currentPushDistributor = AsyncData.Failure(Exception("Failed to change distributor"))),
aInvalidNotificationSettingsState(),
aInvalidNotificationSettingsState(fixFailed = true),
aValidNotificationSettingsState(fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(permissionGranted = false)),
aValidNotificationSettingsState(appNotificationEnabled = false),
)
}
fun aValidNotificationSettingsState(
changeNotificationSettingAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
atRoomNotificationsEnabled: Boolean = true,
callNotificationsEnabled: Boolean = true,
inviteForMeNotificationsEnabled: Boolean = true,
systemNotificationsEnabled: Boolean = true,
appNotificationEnabled: Boolean = true,
currentPushDistributor: AsyncData<Distributor> = AsyncData.Success(aDistributor("Firebase")),
availablePushDistributors: List<Distributor> = listOf(
aDistributor("Firebase"),
aDistributor("ntfy"),
),
showChangePushProviderDialog: Boolean = false,
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
eventSink: (NotificationSettingsEvents) -> Unit = {},
) = NotificationSettingsState(
matrixSettings = NotificationSettingsState.MatrixSettings.Valid(
atRoomNotificationsEnabled = atRoomNotificationsEnabled,
callNotificationsEnabled = callNotificationsEnabled,
inviteForMeNotificationsEnabled = inviteForMeNotificationsEnabled,
defaultGroupNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
defaultOneToOneNotificationMode = RoomNotificationMode.ALL_MESSAGES,
),
appSettings = NotificationSettingsState.AppSettings(
systemNotificationsEnabled = systemNotificationsEnabled,
appNotificationsEnabled = appNotificationEnabled,
),
changeNotificationSettingAction = changeNotificationSettingAction,
currentPushDistributor = currentPushDistributor,
availablePushDistributors = availablePushDistributors.toImmutableList(),
showChangePushProviderDialog = showChangePushProviderDialog,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
eventSink = eventSink,
)
fun aInvalidNotificationSettingsState(
fixFailed: Boolean = false,
eventSink: (NotificationSettingsEvents) -> Unit = {},
) = NotificationSettingsState(
matrixSettings = NotificationSettingsState.MatrixSettings.Invalid(
fixFailed = fixFailed,
),
appSettings = NotificationSettingsState.AppSettings(
systemNotificationsEnabled = false,
appNotificationsEnabled = true,
),
changeNotificationSettingAction = AsyncAction.Uninitialized,
currentPushDistributor = AsyncData.Uninitialized,
availablePushDistributors = persistentListOf(),
showChangePushProviderDialog = false,
fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
eventSink = eventSink,
)
fun aDistributor(
name: String = "Name",
value: String = "$name Value",
) = Distributor(
value = value,
name = name,
)
@@ -0,0 +1,309 @@
/*
* 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.preferences.impl.notifications
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.dialogs.ListOption
import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
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.Text
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList
/**
* A view that allows a user edit their global notification settings.
*/
@Composable
fun NotificationSettingsView(
state: NotificationSettingsState,
onOpenEditDefault: (isOneToOne: Boolean) -> Unit,
onTroubleshootNotificationsClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
else -> Unit
}
}
PreferencePage(
modifier = modifier,
onBackClick = onBackClick,
title = stringResource(id = R.string.screen_notification_settings_title)
) {
when (state.matrixSettings) {
is NotificationSettingsState.MatrixSettings.Invalid -> InvalidNotificationSettingsView(
showError = state.matrixSettings.fixFailed,
onContinueClick = { state.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) },
onDismissError = { state.eventSink(NotificationSettingsEvents.ClearConfigurationMismatchError) },
)
NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferencePage
is NotificationSettingsState.MatrixSettings.Valid -> NotificationSettingsContentView(
matrixSettings = state.matrixSettings,
state = state,
onNotificationsEnabledChange = { state.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(it)) },
onGroupChatsClick = { onOpenEditDefault(false) },
onDirectChatsClick = { onOpenEditDefault(true) },
onMentionNotificationsChange = { state.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(it)) },
// TODO We are removing the call notification toggle until support for call notifications has been added
// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) },
onInviteForMeNotificationsChange = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) },
onTroubleshootNotificationsClick = onTroubleshootNotificationsClick,
)
}
AsyncActionView(
async = state.changeNotificationSettingAction,
errorMessage = { stringResource(R.string.screen_notification_settings_edit_failed_updating_default_mode) },
onErrorDismiss = { state.eventSink(NotificationSettingsEvents.ClearNotificationChangeError) },
onSuccess = {},
)
}
}
@Composable
private fun NotificationSettingsContentView(
matrixSettings: NotificationSettingsState.MatrixSettings.Valid,
state: NotificationSettingsState,
onNotificationsEnabledChange: (Boolean) -> Unit,
onGroupChatsClick: () -> Unit,
onDirectChatsClick: () -> Unit,
onMentionNotificationsChange: (Boolean) -> Unit,
// TODO We are removing the call notification toggle until support for call notifications has been added
// onCallsNotificationsChanged: (Boolean) -> Unit,
onInviteForMeNotificationsChange: (Boolean) -> Unit,
onTroubleshootNotificationsClick: () -> Unit,
) {
val context = LocalContext.current
val systemSettings: NotificationSettingsState.AppSettings = state.appSettings
if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) {
ListItem(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.NotificationsOffSolid())),
headlineContent = {
Text(stringResource(id = R.string.screen_notification_settings_system_notifications_turned_off))
},
supportingContent = {
Text(
stringResource(
id = R.string.screen_notification_settings_system_notifications_action_required,
stringResource(id = R.string.screen_notification_settings_system_notifications_action_required_content_link)
)
)
},
onClick = {
context.startNotificationSettingsIntent()
}
)
}
PreferenceSwitch(
title = stringResource(id = R.string.screen_notification_settings_enable_notifications),
isChecked = systemSettings.appNotificationsEnabled,
onCheckedChange = onNotificationsEnabledChange
)
if (systemSettings.appNotificationsEnabled) {
if (!state.fullScreenIntentPermissionsState.permissionGranted) {
PreferenceCategory {
ListItem(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VoiceCallSolid())),
headlineContent = {
Text(stringResource(id = R.string.full_screen_intent_banner_title))
},
supportingContent = {
Text(stringResource(R.string.full_screen_intent_banner_message))
},
onClick = {
state.fullScreenIntentPermissionsState.eventSink(FullScreenIntentPermissionsEvents.OpenSettings)
}
)
}
}
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_notification_section_title)) {
ListItem(
headlineContent = {
Text(stringResource(id = R.string.screen_notification_settings_group_chats))
},
supportingContent = {
Text(getTitleForRoomNotificationMode(mode = matrixSettings.defaultGroupNotificationMode))
},
onClick = onGroupChatsClick
)
ListItem(
headlineContent = {
Text(stringResource(id = R.string.screen_notification_settings_direct_chats))
},
supportingContent = {
Text(getTitleForRoomNotificationMode(mode = matrixSettings.defaultOneToOneNotificationMode))
},
onClick = onDirectChatsClick
)
}
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_mode_mentions)) {
PreferenceSwitch(
modifier = Modifier,
title = stringResource(id = R.string.screen_notification_settings_room_mention_label),
isChecked = matrixSettings.atRoomNotificationsEnabled,
onCheckedChange = onMentionNotificationsChange
)
}
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_additional_settings_section_title)) {
// TODO We are removing the call notification toggle until support for call notifications has been added
// PreferenceSwitch(
// modifier = Modifier,
// title = stringResource(id = CommonStrings.screen_notification_settings_calls_label),
// isChecked = matrixSettings.callNotificationsEnabled,
// switchAlignment = Alignment.Top,
// onCheckedChange = onCallsNotificationsChanged
// )
PreferenceSwitch(
modifier = Modifier,
title = stringResource(id = R.string.screen_notification_settings_invite_for_me_label),
isChecked = matrixSettings.inviteForMeNotificationsEnabled,
onCheckedChange = onInviteForMeNotificationsChange
)
}
PreferenceCategory(title = stringResource(id = R.string.troubleshoot_notifications_entry_point_section)) {
ListItem(
headlineContent = {
Text(stringResource(id = R.string.troubleshoot_notifications_entry_point_title))
},
onClick = onTroubleshootNotificationsClick
)
}
if (state.showAdvancedSettings) {
PreferenceCategory(title = stringResource(id = CommonStrings.common_advanced_settings)) {
ListItem(
headlineContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_push_provider_android))
},
trailingContent = when (state.currentPushDistributor) {
AsyncData.Uninitialized,
is AsyncData.Loading -> ListItemContent.Custom {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(20.dp),
strokeWidth = 2.dp
)
}
is AsyncData.Failure -> ListItemContent.Text(
stringResource(id = CommonStrings.common_error)
)
is AsyncData.Success -> ListItemContent.Text(
state.currentPushDistributor.dataOrNull()?.name ?: ""
)
},
onClick = {
if (state.currentPushDistributor.isReady()) {
state.eventSink(NotificationSettingsEvents.ChangePushProvider)
}
}
)
}
if (state.showChangePushProviderDialog) {
SingleSelectionDialog(
title = stringResource(id = R.string.screen_advanced_settings_choose_distributor_dialog_title_android),
options = state.availablePushDistributors.map { distributor ->
// If there are several distributors with the same name, use the full name
val title = if (state.availablePushDistributors.count { it.name == distributor.name } > 1) {
distributor.fullName
} else {
distributor.name
}
ListOption(title = title)
}.toImmutableList(),
initialSelection = state.availablePushDistributors.indexOf(state.currentPushDistributor.dataOrNull()),
onSelectOption = { index ->
state.eventSink(
NotificationSettingsEvents.SetPushProvider(index)
)
},
onDismissRequest = { state.eventSink(NotificationSettingsEvents.CancelChangePushProvider) },
)
}
}
}
}
@Composable
private fun getTitleForRoomNotificationMode(mode: RoomNotificationMode?) =
when (mode) {
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages)
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute)
null -> ""
}
@Composable
private fun InvalidNotificationSettingsView(
showError: Boolean,
onContinueClick: () -> Unit,
onDismissError: () -> Unit,
modifier: Modifier = Modifier,
) {
Announcement(
title = stringResource(R.string.screen_notification_settings_configuration_mismatch),
description = stringResource(R.string.screen_notification_settings_configuration_mismatch_description),
type = AnnouncementType.Actionable(
onActionClick = onContinueClick,
actionText = stringResource(CommonStrings.action_continue),
onDismissClick = null,
),
modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
if (showError) {
ErrorDialog(
title = stringResource(id = CommonStrings.dialog_title_error),
content = stringResource(id = R.string.screen_notification_settings_failed_fixing_configuration),
onSubmit = onDismissError
)
}
}
@PreviewsDayNight
@Composable
internal fun NotificationSettingsViewPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = ElementPreview {
NotificationSettingsView(
state = state,
onBackClick = {},
onOpenEditDefault = {},
onTroubleshootNotificationsClick = {},
)
}
@@ -0,0 +1,28 @@
/*
* 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.preferences.impl.notifications
import androidx.core.app.NotificationManagerCompat
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
interface SystemNotificationsEnabledProvider {
fun notificationsEnabled(): Boolean
}
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultSystemNotificationsEnabledProvider(
private val notificationManager: NotificationManagerCompat,
) : SystemNotificationsEnabledProvider {
override fun notificationsEnabled(): Boolean {
return notificationManager.areNotificationsEnabled()
}
}
@@ -0,0 +1,73 @@
/*
* 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.preferences.impl.notifications.edit
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.preferences.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.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@Composable
fun DefaultNotificationSettingOption(
mode: RoomNotificationMode,
onSelectOption: (RoomNotificationMode) -> Unit,
displayMentionsOnlyDisclaimer: Boolean,
modifier: Modifier = Modifier,
isSelected: Boolean = false,
) {
val title = when (mode) {
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages)
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
else -> ""
}
val subtitle = when {
mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY && displayMentionsOnlyDisclaimer -> {
stringResource(id = R.string.screen_notification_settings_mentions_only_disclaimer)
}
else -> null
}
ListItem(
modifier = modifier,
headlineContent = { Text(title) },
supportingContent = subtitle?.let { { Text(it) } },
trailingContent = ListItemContent.RadioButton(selected = isSelected),
onClick = { onSelectOption(mode) },
)
}
@PreviewsDayNight
@Composable
internal fun DefaultNotificationSettingOptionPreview() = ElementPreview {
Column {
DefaultNotificationSettingOption(
mode = RoomNotificationMode.ALL_MESSAGES,
isSelected = true,
displayMentionsOnlyDisclaimer = false,
onSelectOption = {},
)
DefaultNotificationSettingOption(
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
isSelected = false,
displayMentionsOnlyDisclaimer = false,
onSelectOption = {},
)
DefaultNotificationSettingOption(
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
isSelected = false,
displayMentionsOnlyDisclaimer = true,
onSelectOption = {},
)
}
}
@@ -0,0 +1,54 @@
/*
* 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.preferences.impl.notifications.edit
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(SessionScope::class)
@AssistedInject
class EditDefaultNotificationSettingNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: EditDefaultNotificationSettingPresenter.Factory
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToRoomNotificationSettings(roomId: RoomId)
}
data class Inputs(
val isOneToOne: Boolean
) : NodeInputs
private val callback: Callback = callback()
private val inputs = inputs<Inputs>()
private val presenter = presenterFactory.create(inputs.isOneToOne)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
EditDefaultNotificationSettingView(
state = state,
openRoomNotificationSettings = callback::navigateToRoomNotificationSettings,
onBackClick = ::navigateUp,
modifier = modifier,
)
}
}
@@ -0,0 +1,163 @@
/*
* 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.preferences.impl.notifications.edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
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.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.getAvatarData
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.text.Collator
import kotlin.time.Duration.Companion.seconds
@AssistedInject
class EditDefaultNotificationSettingPresenter(
private val notificationSettingsService: NotificationSettingsService,
@Assisted private val isOneToOne: Boolean,
private val roomListService: RoomListService,
) : Presenter<EditDefaultNotificationSettingState> {
@AssistedFactory
interface Factory {
fun create(oneToOne: Boolean): EditDefaultNotificationSettingPresenter
}
private val collator = Collator.getInstance().apply {
decomposition = Collator.CANONICAL_DECOMPOSITION
}
@Composable
override fun present(): EditDefaultNotificationSettingState {
var displayMentionsOnlyDisclaimer by remember { mutableStateOf(false) }
val mode: MutableState<RoomNotificationMode?> = remember {
mutableStateOf(null)
}
val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>> = remember {
mutableStateOf(emptyList())
}
val localCoroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
fetchSettings(mode)
observeNotificationSettings(mode, changeNotificationSettingAction)
observeRoomSummaries(roomsWithUserDefinedMode)
displayMentionsOnlyDisclaimer = !notificationSettingsService.canHomeServerPushEncryptedEventsToDevice().getOrDefault(true)
}
fun handleEvent(event: EditDefaultNotificationSettingStateEvents) {
when (event) {
is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> {
localCoroutineScope.setDefaultNotificationMode(event.mode, changeNotificationSettingAction)
}
EditDefaultNotificationSettingStateEvents.ClearError -> changeNotificationSettingAction.value = AsyncAction.Uninitialized
}
}
return EditDefaultNotificationSettingState(
isOneToOne = isOneToOne,
mode = mode.value,
roomsWithUserDefinedMode = roomsWithUserDefinedMode.value.toImmutableList(),
changeNotificationSettingAction = changeNotificationSettingAction.value,
displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.fetchSettings(mode: MutableState<RoomNotificationMode?>) = launch {
mode.value = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = isOneToOne).getOrThrow()
}
@OptIn(FlowPreview::class)
private fun CoroutineScope.observeNotificationSettings(
mode: MutableState<RoomNotificationMode?>,
changeNotificationSettingAction: MutableState<AsyncAction<Unit>>,
) {
notificationSettingsService.notificationSettingsChangeFlow
.debounce(0.5.seconds)
.onEach {
fetchSettings(mode)
changeNotificationSettingAction.value = AsyncAction.Uninitialized
}
.launchIn(this)
}
private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>) {
roomListService.allRooms
.summaries
.onEach { roomSummaries ->
updateRoomsWithUserDefinedMode(roomSummaries, roomsWithUserDefinedMode)
}
.launchIn(this)
}
private suspend fun updateRoomsWithUserDefinedMode(
summaries: List<RoomSummary>,
roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>
) {
val roomWithUserDefinedRules: Set<RoomId> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrDefault(emptyList()).toSet()
roomsWithUserDefinedMode.value = summaries
.filter { roomSummary ->
roomWithUserDefinedRules.contains(roomSummary.roomId) && roomSummary.isOneToOne == isOneToOne
}
.map { roomSummary ->
EditNotificationSettingRoomInfo(
roomId = roomSummary.roomId,
name = roomSummary.info.name,
heroesAvatar = roomSummary.info.heroes.map { hero ->
hero.getAvatarData(AvatarSize.CustomRoomNotificationSetting)
}.toImmutableList(),
avatarData = roomSummary.info.getAvatarData(AvatarSize.CustomRoomNotificationSetting),
notificationMode = roomSummary.info.userDefinedNotificationMode,
)
}
// locale sensitive sorting
.sortedWith(
compareBy(collator) { roomSummary ->
// Collator does not handle null values, so we provide a fallback
roomSummary.name ?: roomSummary.roomId.value
}
)
}
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch {
action.runUpdatingStateNoSuccess {
// On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did).
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne)
.map {
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne)
}
}
}
}
@@ -0,0 +1,22 @@
/*
* 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.preferences.impl.notifications.edit
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import kotlinx.collections.immutable.ImmutableList
data class EditDefaultNotificationSettingState(
val isOneToOne: Boolean,
val mode: RoomNotificationMode?,
val roomsWithUserDefinedMode: ImmutableList<EditNotificationSettingRoomInfo>,
val changeNotificationSettingAction: AsyncAction<Unit>,
val displayMentionsOnlyDisclaimer: Boolean,
val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit,
)
@@ -0,0 +1,16 @@
/*
* 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.preferences.impl.notifications.edit
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
sealed interface EditDefaultNotificationSettingStateEvents {
data class SetNotificationMode(val mode: RoomNotificationMode) : EditDefaultNotificationSettingStateEvents
data object ClearError : EditDefaultNotificationSettingStateEvents
}
@@ -0,0 +1,59 @@
/*
* 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.preferences.impl.notifications.edit
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
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.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import kotlinx.collections.immutable.persistentListOf
open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> {
override val values: Sequence<EditDefaultNotificationSettingState>
get() = sequenceOf(
anEditDefaultNotificationSettingsState(),
anEditDefaultNotificationSettingsState(isOneToOne = true),
anEditDefaultNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading),
anEditDefaultNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(RuntimeException("error"))),
anEditDefaultNotificationSettingsState(displayMentionsOnlyDisclaimer = true),
)
}
private fun anEditDefaultNotificationSettingsState(
isOneToOne: Boolean = false,
changeNotificationSettingAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
displayMentionsOnlyDisclaimer: Boolean = false,
) = EditDefaultNotificationSettingState(
isOneToOne = isOneToOne,
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
roomsWithUserDefinedMode = persistentListOf(
anEditNotificationSettingRoomInfo("Room"),
anEditNotificationSettingRoomInfo(null),
),
changeNotificationSettingAction = changeNotificationSettingAction,
displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer,
eventSink = {}
)
private fun anEditNotificationSettingRoomInfo(
name: String?,
) = EditNotificationSettingRoomInfo(
roomId = RoomId("!roomId:domain"),
name = name,
avatarData = AvatarData(
id = "!roomId:domain",
name = name,
url = null,
size = AvatarSize.CustomRoomNotificationSetting,
),
heroesAvatar = persistentListOf(),
notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
)
@@ -0,0 +1,135 @@
/*
* 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.preferences.impl.notifications.edit
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.selection.selectableGroup
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.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.ui.strings.CommonStrings
/**
* A view that allows a user to edit the default notification setting for rooms. This can be set separately
* for one-to-one and group rooms, indicated by [EditDefaultNotificationSettingState.isOneToOne].
*/
@Composable
fun EditDefaultNotificationSettingView(
state: EditDefaultNotificationSettingState,
openRoomNotificationSettings: (roomId: RoomId) -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val title = if (state.isOneToOne) {
R.string.screen_notification_settings_direct_chats
} else {
R.string.screen_notification_settings_group_chats
}
PreferencePage(
modifier = modifier,
onBackClick = onBackClick,
title = stringResource(id = title)
) {
// Only ALL_MESSAGES and MENTIONS_AND_KEYWORDS_ONLY are valid global defaults.
val validModes = listOf(RoomNotificationMode.ALL_MESSAGES, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
val categoryTitle = if (state.isOneToOne) {
R.string.screen_notification_settings_edit_screen_direct_section_header
} else {
R.string.screen_notification_settings_edit_screen_group_section_header
}
PreferenceCategory(
title = stringResource(id = categoryTitle),
showTopDivider = false,
) {
if (state.mode != null) {
Column(modifier = Modifier.selectableGroup()) {
validModes.forEach { item ->
DefaultNotificationSettingOption(
mode = item,
isSelected = state.mode == item,
displayMentionsOnlyDisclaimer = state.displayMentionsOnlyDisclaimer,
onSelectOption = { state.eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(it)) }
)
}
}
}
}
if (state.roomsWithUserDefinedMode.isNotEmpty()) {
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) {
state.roomsWithUserDefinedMode.forEach { summary ->
val subtitle = when (summary.notificationMode) {
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages)
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> {
stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
}
RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute)
null -> ""
}
ListItem(
headlineContent = {
val roomName = summary.name
Text(
text = roomName ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { roomName == null }
)
},
supportingContent = {
Text(text = subtitle)
},
leadingContent = ListItemContent.Custom {
Avatar(
avatarData = summary.avatarData,
avatarType = AvatarType.Room(
heroes = summary.heroesAvatar,
),
)
},
onClick = {
openRoomNotificationSettings(summary.roomId)
}
)
}
}
}
AsyncActionView(
async = state.changeNotificationSettingAction,
errorMessage = { stringResource(R.string.screen_notification_settings_edit_failed_updating_default_mode) },
onErrorDismiss = { state.eventSink(EditDefaultNotificationSettingStateEvents.ClearError) },
onSuccess = {},
)
}
}
@PreviewsDayNight
@Composable
internal fun EditDefaultNotificationSettingViewPreview(
@PreviewParameter(EditDefaultNotificationSettingStateProvider::class) state: EditDefaultNotificationSettingState
) = ElementPreview {
EditDefaultNotificationSettingView(
state = state,
openRoomNotificationSettings = {},
onBackClick = {},
)
}
@@ -0,0 +1,22 @@
/*
* 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.preferences.impl.notifications.edit
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import kotlinx.collections.immutable.ImmutableList
data class EditNotificationSettingRoomInfo(
val roomId: RoomId,
val name: String?,
val heroesAvatar: ImmutableList<AvatarData>,
val avatarData: AvatarData,
val notificationMode: RoomNotificationMode?
)
@@ -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.preferences.impl.root
import io.element.android.libraries.matrix.api.core.SessionId
sealed interface PreferencesRootEvents {
data object OnVersionInfoClick : PreferencesRootEvents
data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvents
}
@@ -0,0 +1,104 @@
/*
* 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.preferences.impl.root
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.MatrixUser
@ContributesNode(SessionScope::class)
@AssistedInject
class PreferencesRootNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: PreferencesRootPresenter,
private val directLogoutView: DirectLogoutView,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToAddAccount()
fun navigateToBugReport()
fun navigateToSecureBackup()
fun navigateToAnalyticsSettings()
fun navigateToAbout()
fun navigateToDeveloperSettings()
fun navigateToNotificationSettings()
fun navigateToLockScreenSettings()
fun navigateToAdvancedSettings()
fun navigateToLabs()
fun navigateToUserProfile(matrixUser: MatrixUser)
fun navigateToBlockedUsers()
fun startSignOutFlow()
fun startAccountDeactivationFlow()
}
private val callback: Callback = callback()
private fun onManageAccountClick(
activity: Activity,
url: String?,
isDark: Boolean,
) {
url?.let {
activity.openUrlInChromeCustomTab(
null,
darkTheme = isDark,
url = it
)
}
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
PreferencesRootView(
state = state,
modifier = modifier,
onBackClick = this::navigateUp,
onAddAccountClick = callback::navigateToAddAccount,
onOpenRageShake = callback::navigateToBugReport,
onOpenAnalytics = callback::navigateToAnalyticsSettings,
onOpenAbout = callback::navigateToAbout,
onSecureBackupClick = callback::navigateToSecureBackup,
onOpenDeveloperSettings = callback::navigateToDeveloperSettings,
onOpenAdvancedSettings = callback::navigateToAdvancedSettings,
onOpenLabs = callback::navigateToLabs,
onManageAccountClick = { onManageAccountClick(activity, it, isDark) },
onOpenNotificationSettings = callback::navigateToNotificationSettings,
onOpenLockScreenSettings = callback::navigateToLockScreenSettings,
onOpenUserProfile = callback::navigateToUserProfile,
onOpenBlockedUsers = callback::navigateToBlockedUsers,
onSignOutClick = {
if (state.directLogoutState.canDoDirectSignOut) {
state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
} else {
callback.startSignOutFlow()
}
},
onDeactivateClick = callback::startAccountDeactivationFlow
)
directLogoutView.Render(state = state.directLogoutState)
}
}
@@ -0,0 +1,166 @@
/*
* 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.preferences.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
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.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
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.core.UserId
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@Inject
class PreferencesRootPresenter(
private val matrixClient: MatrixClient,
private val sessionVerificationService: SessionVerificationService,
private val analyticsService: AnalyticsService,
private val versionFormatter: VersionFormatter,
private val snackbarDispatcher: SnackbarDispatcher,
private val indicatorService: IndicatorService,
private val directLogoutPresenter: Presenter<DirectLogoutState>,
private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService,
private val sessionStore: SessionStore,
) : Presenter<PreferencesRootState> {
@Composable
override fun present(): PreferencesRootState {
val coroutineScope = rememberCoroutineScope()
val matrixUser = matrixClient.userProfile.collectAsState()
LaunchedEffect(Unit) {
// Force a refresh of the profile
matrixClient.getUserProfile()
}
val isMultiAccountEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount)
}.collectAsState(initial = false)
val otherSessions by remember {
sessionStore.sessionsFlow().map { list ->
list
.filter { it.userId != matrixClient.sessionId.value }
.map {
MatrixUser(
userId = UserId(it.userId),
displayName = it.userDisplayName,
avatarUrl = it.userAvatarUrl,
)
}
.toImmutableList()
}
}.collectAsState(initial = persistentListOf())
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
val hasAnalyticsProviders = remember { analyticsService.getAvailableAnalyticsProviders().isNotEmpty() }
// We should display the 'complete verification' option if the current session can be verified
val canVerifyUserSession by sessionVerificationService.needsSessionVerification.collectAsState(false)
val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator()
val accountManagementUrl: MutableState<String?> = remember {
mutableStateOf(null)
}
val devicesManagementUrl: MutableState<String?> = remember {
mutableStateOf(null)
}
var canDeactivateAccount by remember {
mutableStateOf(false)
}
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
LaunchedEffect(Unit) {
canDeactivateAccount = matrixClient.canDeactivateAccount()
}
val showBlockedUsersItem by produceState(initialValue = false) {
matrixClient.ignoredUsersFlow
.onEach { value = it.isNotEmpty() }
.launchIn(this)
}
val showLabsItem = remember { featureFlagService.getAvailableFeatures(isInLabs = true).isNotEmpty() }
val directLogoutState = directLogoutPresenter.present()
LaunchedEffect(Unit) {
initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
}
val showDeveloperSettings by showDeveloperSettingsProvider.showDeveloperSettings.collectAsState()
fun handleEvent(event: PreferencesRootEvents) {
when (event) {
is PreferencesRootEvents.OnVersionInfoClick -> {
showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope)
}
is PreferencesRootEvents.SwitchToSession -> coroutineScope.launch {
sessionStore.setLatestSession(event.sessionId.value)
}
}
}
return PreferencesRootState(
myUser = matrixUser.value,
version = versionFormatter.get(),
deviceId = matrixClient.deviceId,
isMultiAccountEnabled = isMultiAccountEnabled,
otherSessions = otherSessions,
showSecureBackup = !canVerifyUserSession,
showSecureBackupBadge = showSecureBackupIndicator,
accountManagementUrl = accountManagementUrl.value,
devicesManagementUrl = devicesManagementUrl.value,
showAnalyticsSettings = hasAnalyticsProviders,
canReportBug = canReportBug,
showDeveloperSettings = showDeveloperSettings,
canDeactivateAccount = canDeactivateAccount,
showBlockedUsersItem = showBlockedUsersItem,
showLabsItem = showLabsItem,
directLogoutState = directLogoutState,
snackbarMessage = snackbarMessage,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.initAccountManagementUrl(
accountManagementUrl: MutableState<String?>,
devicesManagementUrl: MutableState<String?>,
) = launch {
accountManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.Profile).getOrNull()
devicesManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.SessionsList).getOrNull()
}
}
@@ -0,0 +1,36 @@
/*
* 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.preferences.impl.root
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.core.DeviceId
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class PreferencesRootState(
val myUser: MatrixUser,
val version: String,
val deviceId: DeviceId?,
val isMultiAccountEnabled: Boolean,
val otherSessions: ImmutableList<MatrixUser>,
val showSecureBackup: Boolean,
val showSecureBackupBadge: Boolean,
val accountManagementUrl: String?,
val devicesManagementUrl: String?,
val canReportBug: Boolean,
val showAnalyticsSettings: Boolean,
val showDeveloperSettings: Boolean,
val canDeactivateAccount: Boolean,
val showBlockedUsersItem: Boolean,
val showLabsItem: Boolean,
val directLogoutState: DirectLogoutState,
val snackbarMessage: SnackbarMessage?,
val eventSink: (PreferencesRootEvents) -> Unit,
)
@@ -0,0 +1,42 @@
/*
* 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.preferences.impl.root
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.DeviceId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList
fun aPreferencesRootState(
myUser: MatrixUser = aMatrixUser(),
otherSessions: List<MatrixUser> = emptyList(),
eventSink: (PreferencesRootEvents) -> Unit = { _ -> },
) = PreferencesRootState(
myUser = myUser,
version = "Version 1.1 (1)",
deviceId = DeviceId("ILAKNDNASDLK"),
isMultiAccountEnabled = true,
otherSessions = otherSessions.toImmutableList(),
showSecureBackup = true,
showSecureBackupBadge = true,
accountManagementUrl = "aUrl",
devicesManagementUrl = "anOtherUrl",
showAnalyticsSettings = true,
canReportBug = true,
showDeveloperSettings = true,
showBlockedUsersItem = true,
showLabsItem = true,
canDeactivateAccount = true,
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
directLogoutState = aDirectLogoutState(),
eventSink = eventSink,
)
@@ -0,0 +1,376 @@
/*
* 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.preferences.impl.root
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.preferences.impl.R
import io.element.android.features.preferences.impl.user.UserPreferences
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
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.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Text
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.DeviceId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserProvider
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun PreferencesRootView(
state: PreferencesRootState,
onBackClick: () -> Unit,
onAddAccountClick: () -> Unit,
onSecureBackupClick: () -> Unit,
onManageAccountClick: (url: String) -> Unit,
onOpenAnalytics: () -> Unit,
onOpenRageShake: () -> Unit,
onOpenLockScreenSettings: () -> Unit,
onOpenAbout: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onOpenAdvancedSettings: () -> Unit,
onOpenLabs: () -> Unit,
onOpenNotificationSettings: () -> Unit,
onOpenUserProfile: (MatrixUser) -> Unit,
onOpenBlockedUsers: () -> Unit,
onSignOutClick: () -> Unit,
onDeactivateClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
// Include pref from other modules
PreferencePage(
modifier = modifier,
onBackClick = onBackClick,
title = stringResource(id = CommonStrings.common_settings),
snackbarHost = { SnackbarHost(snackbarHostState) }
) {
UserPreferences(
modifier = Modifier.clickable {
onOpenUserProfile(state.myUser)
},
user = state.myUser,
)
if (state.isMultiAccountEnabled) {
MultiAccountSection(
state = state,
onAddAccountClick = onAddAccountClick,
)
}
// 'Manage my app' section
ManageAppSection(
state = state,
onOpenNotificationSettings = onOpenNotificationSettings,
onOpenLockScreenSettings = onOpenLockScreenSettings,
onSecureBackupClick = onSecureBackupClick,
)
// 'Account' section
ManageAccountSection(
state = state,
onManageAccountClick = onManageAccountClick,
onOpenBlockedUsers = onOpenBlockedUsers
)
// General section
GeneralSection(
state = state,
onOpenAbout = onOpenAbout,
onOpenAnalytics = onOpenAnalytics,
onOpenRageShake = onOpenRageShake,
onOpenAdvancedSettings = onOpenAdvancedSettings,
onOpenDeveloperSettings = onOpenDeveloperSettings,
onOpenLabs = onOpenLabs,
onSignOutClick = onSignOutClick,
onDeactivateClick = onDeactivateClick,
)
Footer(
version = state.version,
deviceId = state.deviceId,
onClick = if (!state.showDeveloperSettings) {
{ state.eventSink(PreferencesRootEvents.OnVersionInfoClick) }
} else {
null
}
)
}
}
@Composable
private fun ColumnScope.MultiAccountSection(
state: PreferencesRootState,
onAddAccountClick: () -> Unit,
) {
HorizontalDivider(
thickness = 8.dp,
color = ElementTheme.colors.bgSubtleSecondary,
)
state.otherSessions.forEach { matrixUser ->
MatrixUserRow(
modifier = Modifier.clickable {
state.eventSink(PreferencesRootEvents.SwitchToSession(matrixUser.userId))
},
matrixUser = matrixUser,
avatarSize = AvatarSize.AccountItem,
)
HorizontalDivider()
}
ListItem(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())),
headlineContent = {
Text(stringResource(CommonStrings.common_add_another_account))
},
onClick = onAddAccountClick,
)
HorizontalDivider(
thickness = 8.dp,
color = ElementTheme.colors.bgSubtleSecondary,
)
}
@Composable
private fun ColumnScope.ManageAppSection(
state: PreferencesRootState,
onOpenNotificationSettings: () -> Unit,
onOpenLockScreenSettings: () -> Unit,
onSecureBackupClick: () -> Unit,
) {
ListItem(
headlineContent = { Text(stringResource(id = R.string.screen_notification_settings_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())),
onClick = onOpenNotificationSettings,
)
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_screen_lock)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
onClick = onOpenLockScreenSettings,
)
if (state.showSecureBackup) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_encryption)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Key())),
trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge },
onClick = onSecureBackupClick,
)
}
HorizontalDivider()
}
@Composable
private fun ColumnScope.ManageAccountSection(
state: PreferencesRootState,
onManageAccountClick: (url: String) -> Unit,
onOpenBlockedUsers: () -> Unit,
) {
state.accountManagementUrl?.let { url ->
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.action_manage_account)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())),
onClick = { onManageAccountClick(url) },
)
}
state.devicesManagementUrl?.let { url ->
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.action_manage_devices)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Devices())),
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())),
onClick = { onManageAccountClick(url) },
)
}
if (state.showBlockedUsersItem) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
onClick = onOpenBlockedUsers,
)
}
if (state.accountManagementUrl != null || state.devicesManagementUrl != null || state.showBlockedUsersItem) {
HorizontalDivider()
}
}
@Composable
private fun ColumnScope.GeneralSection(
state: PreferencesRootState,
onOpenAbout: () -> Unit,
onOpenAnalytics: () -> Unit,
onOpenRageShake: () -> Unit,
onOpenAdvancedSettings: () -> Unit,
onOpenLabs: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onSignOutClick: () -> Unit,
onDeactivateClick: () -> Unit,
) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_about)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
onClick = onOpenAbout,
)
if (state.canReportBug) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
onClick = onOpenRageShake
)
}
if (state.showAnalyticsSettings) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_analytics)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Chart())),
onClick = onOpenAnalytics,
)
}
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_advanced_settings)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Settings())),
onClick = onOpenAdvancedSettings,
)
if (state.showLabsItem) {
ListItem(
headlineContent = { Text(stringResource(id = R.string.screen_labs_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Labs())),
onClick = onOpenLabs,
)
}
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.SignOut())),
style = ListItemStyle.Destructive,
onClick = onSignOutClick,
)
if (state.canDeactivateAccount) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.action_deactivate_account)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Warning())),
style = ListItemStyle.Destructive,
onClick = onDeactivateClick,
)
}
// Put developer settings at the end, so nothing bad happens if the user clicks 8 times to enable the entry
if (state.showDeveloperSettings) {
DeveloperPreferencesView(onOpenDeveloperSettings)
}
}
@Composable
private fun ColumnScope.Footer(
version: String,
deviceId: DeviceId?,
onClick: (() -> Unit)?,
) {
val text = remember(version, deviceId) {
buildString {
append(version)
if (deviceId != null) {
append("\n")
append(deviceId)
}
}
}
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 16.dp)
.clickable(enabled = onClick != null, onClick = onClick ?: {})
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 24.dp),
textAlign = TextAlign.Center,
text = text,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
}
@Composable
private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_developer_options)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Code())),
onClick = onOpenDeveloperSettings
)
}
@PreviewWithLargeHeight
@Composable
internal fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewLight { ContentToPreview(matrixUser) }
@PreviewWithLargeHeight
@Composable
internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewDark { ContentToPreview(matrixUser) }
@ExcludeFromCoverage
@Composable
private fun ContentToPreview(matrixUser: MatrixUser) {
PreferencesRootView(
state = aPreferencesRootState(myUser = matrixUser),
onBackClick = {},
onAddAccountClick = {},
onOpenAnalytics = {},
onOpenRageShake = {},
onOpenDeveloperSettings = {},
onOpenAdvancedSettings = {},
onOpenLabs = {},
onOpenAbout = {},
onSecureBackupClick = {},
onManageAccountClick = {},
onOpenNotificationSettings = {},
onOpenLockScreenSettings = {},
onOpenUserProfile = {},
onOpenBlockedUsers = {},
onSignOutClick = {},
onDeactivateClick = {},
)
}
@PreviewsDayNight
@Composable
internal fun MultiAccountSectionPreview() = ElementPreview {
Column {
MultiAccountSection(
state = aPreferencesRootState(
otherSessions = aMatrixUserList(),
),
onAddAccountClick = {},
)
}
}
@@ -0,0 +1,39 @@
/*
* 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.preferences.impl.root
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
interface VersionFormatter {
fun get(): String
}
@ContributesBinding(AppScope::class)
class DefaultVersionFormatter(
private val stringProvider: StringProvider,
private val buildMeta: BuildMeta,
) : VersionFormatter {
override fun get(): String {
val base = stringProvider.getString(
CommonStrings.settings_version_number,
buildMeta.versionName,
buildMeta.versionCode.toString()
)
return if (buildMeta.gitBranchName == "main") {
base
} else {
// In case of a build not from main, we display the branch name and the revision
"$base\n${buildMeta.gitBranchName} (${buildMeta.gitRevision})"
}
}
}
@@ -0,0 +1,63 @@
/*
* 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.preferences.impl.tasks
import android.content.Context
import coil3.SingletonImageLoader
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Provider
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.preferences.impl.DefaultCacheService
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.api.PushService
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
interface ClearCacheUseCase {
suspend operator fun invoke()
}
@ContributesBinding(SessionScope::class)
class DefaultClearCacheUseCase(
@ApplicationContext private val context: Context,
private val matrixClient: MatrixClient,
private val coroutineDispatchers: CoroutineDispatchers,
private val defaultCacheService: DefaultCacheService,
private val okHttpClient: Provider<OkHttpClient>,
private val pushService: PushService,
private val seenInvitesStore: SeenInvitesStore,
private val activeRoomsHolder: ActiveRoomsHolder,
) : ClearCacheUseCase {
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
// Active rooms should be disposed of before clearing the cache
activeRoomsHolder.clear(matrixClient.sessionId)
// Clear Matrix cache
matrixClient.clearCache()
// Clear Coil cache
SingletonImageLoader.get(context).let {
it.diskCache?.clear()
it.memoryCache?.clear()
}
// Clear OkHttp cache
okHttpClient().cache?.delete()
// Clear app cache
context.cacheDir.deleteRecursively()
// Clear some settings
seenInvitesStore.clear()
// Ensure any error will be displayed again
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)
pushService.resetBatteryOptimizationState()
// Ensure the app is restarted
defaultCacheService.onClearedCache(matrixClient.sessionId)
}
}
@@ -0,0 +1,39 @@
/*
* 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.preferences.impl.tasks
import android.content.Context
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.file.getSizeOfFiles
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.withContext
interface ComputeCacheSizeUseCase {
suspend operator fun invoke(): String
}
@ContributesBinding(SessionScope::class)
class DefaultComputeCacheSizeUseCase(
@ApplicationContext private val context: Context,
private val matrixClient: MatrixClient,
private val coroutineDispatchers: CoroutineDispatchers,
private val fileSizeFormatter: FileSizeFormatter,
) : ComputeCacheSizeUseCase {
override suspend fun invoke(): String = withContext(coroutineDispatchers.io) {
var cumulativeSize = 0L
cumulativeSize += matrixClient.getCacheSize()
// - 4096 to not include the size fo the folder
cumulativeSize += (context.cacheDir.getSizeOfFiles() - 4096).coerceAtLeast(0)
fileSizeFormatter.format(cumulativeSize)
}
}
@@ -0,0 +1,35 @@
/*
* 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.preferences.impl.user
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserHeader
import io.element.android.libraries.matrix.ui.components.MatrixUserWithNullProvider
@Composable
fun UserPreferences(
user: MatrixUser?,
modifier: Modifier = Modifier,
) {
MatrixUserHeader(
modifier = modifier,
matrixUser = user
)
}
@PreviewsDayNight
@Composable
internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserWithNullProvider::class) matrixUser: MatrixUser?) = ElementPreview {
UserPreferences(matrixUser)
}
@@ -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.
*/
package io.element.android.features.preferences.impl.user.editprofile
import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface EditUserProfileEvents {
data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvents
data class UpdateDisplayName(val name: String) : EditUserProfileEvents
data object Exit : EditUserProfileEvents
data object Save : EditUserProfileEvents
data object CloseDialog : EditUserProfileEvents
}
@@ -0,0 +1,12 @@
/*
* 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.preferences.impl.user.editprofile
interface EditUserProfileNavigator {
fun close()
}
@@ -0,0 +1,59 @@
/*
* 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.preferences.impl.user.editprofile
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.MatrixUser
@ContributesNode(SessionScope::class)
@AssistedInject
class EditUserProfileNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: EditUserProfilePresenter.Factory,
) : Node(buildContext, plugins = plugins),
EditUserProfileNavigator {
data class Inputs(
val matrixUser: MatrixUser
) : NodeInputs
interface Callback : Plugin {
fun onDone()
}
val matrixUser = inputs<Inputs>().matrixUser
val callback: Callback = callback()
val presenter = presenterFactory.create(
matrixUser = matrixUser,
navigator = this,
)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
EditUserProfileView(
state = state,
onEditProfileSuccess = ::close,
modifier = modifier
)
}
override fun close() = callback.onDone()
}
@@ -0,0 +1,219 @@
/*
* 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.preferences.impl.user.editprofile
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@AssistedInject
class EditUserProfilePresenter(
@Assisted private val matrixUser: MatrixUser,
@Assisted private val navigator: EditUserProfileNavigator,
private val matrixClient: MatrixClient,
private val mediaPickerProvider: PickerProvider,
private val mediaPreProcessor: MediaPreProcessor,
private val temporaryUriDeleter: TemporaryUriDeleter,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
permissionsPresenterFactory: PermissionsPresenter.Factory,
) : Presenter<EditUserProfileState> {
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
private var pendingPermissionRequest = false
@AssistedFactory
interface Factory {
fun create(
matrixUser: MatrixUser,
navigator: EditUserProfileNavigator,
): EditUserProfilePresenter
}
@Composable
override fun present(): EditUserProfileState {
val cameraPermissionState = cameraPermissionPresenter.present()
var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl) }
var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) }
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri ->
if (uri != null) {
temporaryUriDeleter.delete(userAvatarUri?.toUri())
userAvatarUri = uri.toString()
}
}
)
val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(
onResult = { uri ->
if (uri != null) {
temporaryUriDeleter.delete(userAvatarUri?.toUri())
userAvatarUri = uri.toString()
}
}
)
val avatarActions by remember(userAvatarUri) {
derivedStateOf {
listOfNotNull(
AvatarAction.TakePhoto,
AvatarAction.ChoosePhoto,
AvatarAction.Remove.takeIf { userAvatarUri != null },
).toImmutableList()
}
}
LaunchedEffect(cameraPermissionState.permissionGranted) {
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
pendingPermissionRequest = false
cameraPhotoPicker.launch()
}
}
val saveAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val localCoroutineScope = rememberCoroutineScope()
val canSave = remember(userDisplayName, userAvatarUri) {
val hasProfileChanged = hasDisplayNameChanged(userDisplayName, matrixUser) ||
hasAvatarUrlChanged(userAvatarUri, matrixUser)
!userDisplayName.isNullOrBlank() && hasProfileChanged
}
fun handleEvent(event: EditUserProfileEvents) {
when (event) {
is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(
name = userDisplayName,
avatarUri = userAvatarUri?.toUri(),
currentUser = matrixUser,
action = saveAction,
)
is EditUserProfileEvents.HandleAvatarAction -> {
when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
cameraPhotoPicker.launch()
} else {
pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
}
AvatarAction.Remove -> {
temporaryUriDeleter.delete(userAvatarUri?.toUri())
userAvatarUri = null
}
}
}
is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name
EditUserProfileEvents.Exit -> {
when (saveAction.value) {
is AsyncAction.Confirming -> {
// Close the dialog right now
saveAction.value = AsyncAction.Uninitialized
navigator.close()
}
AsyncAction.Loading -> Unit
is AsyncAction.Failure,
is AsyncAction.Success -> {
// Should not happen
}
AsyncAction.Uninitialized -> {
if (canSave) {
saveAction.value = AsyncAction.ConfirmingCancellation
} else {
navigator.close()
}
}
}
}
EditUserProfileEvents.CloseDialog -> saveAction.value = AsyncAction.Uninitialized
}
}
return EditUserProfileState(
userId = matrixUser.userId,
displayName = userDisplayName.orEmpty(),
userAvatarUrl = userAvatarUri,
avatarActions = avatarActions,
saveButtonEnabled = canSave && saveAction.value !is AsyncAction.Loading,
saveAction = saveAction.value,
cameraPermissionState = cameraPermissionState,
eventSink = ::handleEvent,
)
}
private fun hasDisplayNameChanged(name: String?, currentUser: MatrixUser) =
name?.trim() != currentUser.displayName?.trim()
private fun hasAvatarUrlChanged(avatarUri: String?, currentUser: MatrixUser) =
avatarUri?.trim() != currentUser.avatarUrl?.trim()
private fun CoroutineScope.saveChanges(
name: String?,
avatarUri: Uri?,
currentUser: MatrixUser,
action: MutableState<AsyncAction<Unit>>,
) = launch {
val results = mutableListOf<Result<Unit>>()
suspend {
if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) {
results.add(matrixClient.setDisplayName(name).onFailure {
Timber.e(it, "Failed to set user's display name")
})
}
if (avatarUri?.toString()?.trim() != currentUser.avatarUrl?.trim()) {
results.add(updateAvatar(avatarUri).onFailure {
Timber.e(it, "Failed to update user's avatar")
})
}
if (results.all { it.isSuccess }) Unit else results.first { it.isFailure }.getOrThrow()
}.runCatchingUpdatingState(action)
}
private suspend fun updateAvatar(avatarUri: Uri?): Result<Unit> {
return runCatchingExceptions {
if (avatarUri != null) {
val preprocessed = mediaPreProcessor.process(
uri = avatarUri,
mimeType = MimeTypes.Jpeg,
deleteOriginal = false,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
).getOrThrow()
matrixClient.uploadAvatar(MimeTypes.Jpeg, preprocessed.file.readBytes()).getOrThrow()
} else {
matrixClient.removeAvatar().getOrThrow()
}
}.onFailure { Timber.e(it, "Unable to update avatar") }
}
}
@@ -0,0 +1,26 @@
/*
* 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.preferences.impl.user.editprofile
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList
data class EditUserProfileState(
val userId: UserId,
val displayName: String,
val userAvatarUrl: String?,
val avatarActions: ImmutableList<AvatarAction>,
val saveButtonEnabled: Boolean,
val saveAction: AsyncAction<Unit>,
val cameraPermissionState: PermissionsState,
val eventSink: (EditUserProfileEvents) -> Unit
)
@@ -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.preferences.impl.user.editprofile
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.permissions.api.PermissionsState
import io.element.android.libraries.permissions.api.aPermissionsState
import kotlinx.collections.immutable.toImmutableList
open class EditUserProfileStateProvider : PreviewParameterProvider<EditUserProfileState> {
override val values: Sequence<EditUserProfileState>
get() = sequenceOf(
aEditUserProfileState(),
aEditUserProfileState(userAvatarUrl = "example://uri"),
aEditUserProfileState(saveAction = AsyncAction.ConfirmingCancellation),
)
}
fun aEditUserProfileState(
userId: UserId = UserId("@john.doe:matrix.org"),
displayName: String = "John Doe",
userAvatarUrl: String? = null,
avatarActions: List<AvatarAction> = emptyList(),
saveButtonEnabled: Boolean = true,
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
eventSink: (EditUserProfileEvents) -> Unit = {},
) = EditUserProfileState(
userId = userId,
displayName = displayName,
userAvatarUrl = userAvatarUrl,
avatarActions = avatarActions.toImmutableList(),
saveButtonEnabled = saveButtonEnabled,
saveAction = saveAction,
cameraPermissionState = cameraPermissionState,
eventSink = eventSink,
)
@@ -0,0 +1,175 @@
/*
* 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.preferences.impl.user.editprofile
import androidx.activity.compose.BackHandler
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.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
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.features.preferences.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.SaveChangesDialog
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.EditableAvatarView
import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditUserProfileView(
state: EditUserProfileState,
onEditProfileSuccess: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
fun onAvatarClick() {
focusManager.clearFocus()
isAvatarActionsSheetVisible.value = true
}
fun onBackClick() {
focusManager.clearFocus()
state.eventSink(EditUserProfileEvents.Exit)
}
BackHandler(
enabled = true,
::onBackClick,
)
Scaffold(
modifier = modifier.clearFocusOnTap(focusManager),
topBar = {
TopAppBar(
titleStr = stringResource(R.string.screen_edit_profile_title),
navigationIcon = { BackButton(::onBackClick) },
actions = {
TextButton(
text = stringResource(CommonStrings.action_save),
enabled = state.saveButtonEnabled,
onClick = {
focusManager.clearFocus()
state.eventSink(EditUserProfileEvents.Save)
},
)
}
)
},
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.padding(horizontal = 16.dp)
.navigationBarsPadding()
.imePadding()
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(24.dp))
EditableAvatarView(
matrixId = state.userId.value,
displayName = state.displayName,
avatarUrl = state.userAvatarUrl,
avatarSize = AvatarSize.EditProfileDetails,
avatarType = AvatarType.User,
onAvatarClick = { onAvatarClick() },
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text = state.userId.value,
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(40.dp))
TextField(
label = stringResource(R.string.screen_edit_profile_display_name),
value = state.displayName,
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
singleLine = true,
onValueChange = { state.eventSink(EditUserProfileEvents.UpdateDisplayName(it)) },
)
}
AvatarActionBottomSheet(
actions = state.avatarActions,
isVisible = isAvatarActionsSheetVisible.value,
onDismiss = { isAvatarActionsSheetVisible.value = false },
onSelectAction = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) }
)
AsyncActionView(
async = state.saveAction,
progressDialog = {
AsyncActionViewDefaults.ProgressDialog(
progressText = stringResource(R.string.screen_edit_profile_updating_details),
)
},
confirmationDialog = { confirming ->
when (confirming) {
is AsyncAction.ConfirmingCancellation -> {
SaveChangesDialog(
onSubmitClick = { state.eventSink(EditUserProfileEvents.Exit) },
onDismiss = { state.eventSink(EditUserProfileEvents.CloseDialog) }
)
}
}
},
onSuccess = { onEditProfileSuccess() },
errorTitle = { stringResource(R.string.screen_edit_profile_error_title) },
errorMessage = { stringResource(R.string.screen_edit_profile_error) },
onErrorDismiss = { state.eventSink(EditUserProfileEvents.CloseDialog) },
)
}
PermissionsView(
state = state.cameraPermissionState,
)
}
@PreviewsDayNight
@Composable
internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) =
ElementPreview {
EditUserProfileView(
onEditProfileSuccess = {},
state = state,
)
}
@@ -0,0 +1,38 @@
/*
* 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.preferences.impl.utils
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.ui.utils.MultipleTapToUnlock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@Inject
class ShowDeveloperSettingsProvider(
buildMeta: BuildMeta,
) {
companion object {
const val DEVELOPER_SETTINGS_COUNTER = 7
}
private val multipleTapToUnlock = MultipleTapToUnlock(DEVELOPER_SETTINGS_COUNTER)
private val isDeveloperBuild = buildMeta.buildType != BuildType.RELEASE
private val _showDeveloperSettings = MutableStateFlow(isDeveloperBuild)
val showDeveloperSettings: StateFlow<Boolean> = _showDeveloperSettings
fun unlockDeveloperSettings(scope: CoroutineScope) {
if (multipleTapToUnlock.unlock(scope)) {
_showDeveloperSettings.value = true
}
}
}
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны."</string>
<string name="full_screen_intent_banner_title">"Палепшыце якасць званкоў"</string>
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Выберыце спосаб атрымання апавяшчэнняў"</string>
<string name="screen_advanced_settings_developer_mode">"Рэжым распрацоўшчыка"</string>
<string name="screen_advanced_settings_developer_mode_description">"Падайце распрацоўнікам доступ да функцый і функцыянальным магчымасцям."</string>
<string name="screen_advanced_settings_element_call_base_url">"Карыстальніцкі URL сервера Element Call"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Усталюйце карыстальніцкі асноўны URL для Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Адрас пазначаны няправільна, пераканайцеся, што вы ўказалі пратакол (http/https) і правільны адрас."</string>
<string name="screen_advanced_settings_push_provider_android">"Пастаўшчык push-апавяшчэнняў"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Адключыць рэдактар фарматаванага тэксту і ўключыць Markdown."</string>
<string name="screen_advanced_settings_send_read_receipts">"Апавяшчэнні аб чытанні"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Калі выключыць, вашы пасведчанні аб прачытанні нікому не будуць адпраўляцца. Вы па-ранейшаму будзеце атрымліваць пасведчанні аб прачытанні ад іншых карыстальнікаў."</string>
<string name="screen_advanced_settings_share_presence">"Падзяліцеся прысутнасцю"</string>
<string name="screen_advanced_settings_share_presence_description">"Калі гэта выключана, вы не зможаце адпраўляць або атрымліваць апавяшчэнні аб прачытанні або апавяшчэнні аб наборы тэксту"</string>
<string name="screen_advanced_settings_view_source_description">"Уключыце опцыю для прагляду паведамленняў у хроніцы."</string>
<string name="screen_blocked_users_empty">"У вас няма заблакіраваных карыстальнікаў"</string>
<string name="screen_blocked_users_unblock_alert_action">"Разблакіраваць"</string>
<string name="screen_blocked_users_unblock_alert_description">"Вы зноў зможаце ўбачыць усе паведамленні."</string>
<string name="screen_blocked_users_unblock_alert_title">"Разблакіраваць карыстальніка"</string>
<string name="screen_blocked_users_unblocking">"Разблакіроўка…"</string>
<string name="screen_edit_profile_display_name">"Бачнае імя"</string>
<string name="screen_edit_profile_display_name_placeholder">"Ваша бачнае імя"</string>
<string name="screen_edit_profile_error">"Узнікла невядомая памылка, і інфармацыю не ўдалося змяніць."</string>
<string name="screen_edit_profile_error_title">"Немагчыма абнавіць профіль"</string>
<string name="screen_edit_profile_title">"Рэдагаваць профіль"</string>
<string name="screen_edit_profile_updating_details">"Абнаўленне профілю…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Дадатковыя налады"</string>
<string name="screen_notification_settings_calls_label">"Аўдыя і відэа званкі"</string>
<string name="screen_notification_settings_configuration_mismatch">"Неадпаведнасць канфігурацыі"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Мы спрасцілі налады апавяшчэнняў, каб спрасціць пошук опцый. Некаторыя карыстальніцкія наладкі, абраныя вамі раней, не адлюстроўваюцца ў дадзеным меню, але яны ўсё яшчэ актыўныя.
Калі вы працягнеце, некаторыя налады могуць быць зменены."</string>
<string name="screen_notification_settings_direct_chats">"Прамыя чаты"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Карыстальніцкія налады для кожнага чата"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Пры абнаўленні налад апавяшчэнняў адбылася памылка."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Усе паведамленні"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Толькі згадванні і ключавыя словы"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Апавяшчаць мяне ў асабістых чатах"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Апавяшчаць мяне ў групавых чатах"</string>
<string name="screen_notification_settings_enable_notifications">"Уключыць апавяшчэнні на гэтай прыладзе"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Канфігурацыя не была выпраўлена, паспрабуйце яшчэ раз."</string>
<string name="screen_notification_settings_group_chats">"Групавыя чаты"</string>
<string name="screen_notification_settings_invite_for_me_label">"Запрашэнні"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш хатні сервер не падтрымлівае гэтую опцыю ў зашыфраваных пакоях, вы можаце не атрымаць апавяшчэнне ў некаторых пакоях."</string>
<string name="screen_notification_settings_mentions_section_title">"Згадванні"</string>
<string name="screen_notification_settings_mode_all">"Усе"</string>
<string name="screen_notification_settings_mode_mentions">"Згадванні"</string>
<string name="screen_notification_settings_notification_section_title">"Апавясціць мяне"</string>
<string name="screen_notification_settings_room_mention_label">"Апавясціць пра @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Каб атрымліваць апавяшчэнні, змяніце свой %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"налады сістэмы"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Сістэмныя апавяшчэнні выключаны"</string>
<string name="screen_notification_settings_title">"Апавяшчэнні"</string>
<string name="troubleshoot_notifications_entry_point_section">"Выпраўленне непаладак"</string>
<string name="troubleshoot_notifications_entry_point_title">"Выпраўленне непаладак з апавяшчэннямі"</string>
</resources>
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Изберете как да получавате известия"</string>
<string name="screen_advanced_settings_developer_mode">"Режим за програмисти"</string>
<string name="screen_advanced_settings_developer_mode_description">"Активирайте, за да имате достъп до функции и функционалности за програмисти."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Скриване на профилните снимки в заявките за покана за стая"</string>
<string name="screen_advanced_settings_labs">"Експерименти"</string>
<string name="screen_advanced_settings_media_compression_description">"Качвайте снимки и видеоклипове по-бързо и намалете използването на данни"</string>
<string name="screen_advanced_settings_media_compression_title">"Оптимизиране на качеството на медията"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Модерация и безопасност"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Изключете редактора за форматиран текст, за да пишете Markdown ръчно."</string>
<string name="screen_advanced_settings_send_read_receipts">"Потвърждения за прочитане"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Ако е изключено, вашите потвърждения за прочитане няма да бъдат изпращани на никого. Все още ще получавате потвърждения за прочитане от други потребители."</string>
<string name="screen_advanced_settings_share_presence">"Споделяне на присъствието"</string>
<string name="screen_advanced_settings_share_presence_description">"Ако е изключено, няма да можете да изпращате или получавате потвърждения за прочитане или известия за писане."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Скриване винаги"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Показване винаги"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"В частни стаи"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Скрита мултимедия винаги може да бъде показана, като се докосне"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Показване на мултимедия в хронологията"</string>
<string name="screen_advanced_settings_view_source_description">"Активиране на опцията за преглед на изходния код на съобщението в хронологията."</string>
<string name="screen_blocked_users_unblock_alert_action">"Отблокиране"</string>
<string name="screen_blocked_users_unblock_alert_description">"Ще можете да виждате отново всички съобщения от тях."</string>
<string name="screen_blocked_users_unblock_alert_title">"Отблокиране на потребителя"</string>
<string name="screen_edit_profile_display_name">"Име"</string>
<string name="screen_edit_profile_display_name_placeholder">"Вашето Име"</string>
<string name="screen_edit_profile_error">"Възникна неизвестна грешка и информацията не можа да бъде променена."</string>
<string name="screen_edit_profile_error_title">"Не може да се обнови профила"</string>
<string name="screen_edit_profile_title">"Редактиране на профила"</string>
<string name="screen_edit_profile_updating_details">"Обновяване на профила…"</string>
<string name="screen_labs_enable_threads">"Включване на отговори в нишка"</string>
<string name="screen_labs_header_title">"Искате ли да експериментирате?"</string>
<string name="screen_labs_title">"Експерименти"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Допълнителни настройки"</string>
<string name="screen_notification_settings_calls_label">"Аудио и видео разговори"</string>
<string name="screen_notification_settings_configuration_mismatch">"Несъответствие в конфигурацията"</string>
<string name="screen_notification_settings_direct_chats">"Директни чатове"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Персонализирана настройка за чат"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Възникна грешка при обновяването на настройките за известия."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Всички съобщения"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Само споменавания и ключови думи"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"В директни чатове да бъда известяван за"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"В групови чатове да бъда известяван за"</string>
<string name="screen_notification_settings_enable_notifications">"Включване на известията на това устройство"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Конфигурацията не е оправена, моля, опитайте отново."</string>
<string name="screen_notification_settings_group_chats">"Групови чатове"</string>
<string name="screen_notification_settings_invite_for_me_label">"Покани"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Вашият сървър не поддържа тази опция в шифровани стаи, може да не получавате известия в някои стаи."</string>
<string name="screen_notification_settings_mentions_section_title">"Споменавания"</string>
<string name="screen_notification_settings_mode_mentions">"Споменавания"</string>
<string name="screen_notification_settings_notification_section_title">"Да бъда известяван за"</string>
<string name="screen_notification_settings_room_mention_label">"Известяване за @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"За да получавате известия, моля, променете своя %1$s"</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"системни настройки"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Системните известия са изключени"</string>
<string name="screen_notification_settings_title">"Известия"</string>
<string name="troubleshoot_notifications_entry_point_section">"Отстраняване на неизправности"</string>
<string name="troubleshoot_notifications_entry_point_title">"Отстраняване на неизправности с известията"</string>
</resources>
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Vyberte, jak chcete přijímat oznámení"</string>
<string name="screen_advanced_settings_developer_mode">"Vývojářský režim"</string>
<string name="screen_advanced_settings_developer_mode_description">"Povolením získáte přístup k funkcím a funkcím pro vývojáře."</string>
<string name="screen_advanced_settings_element_call_base_url">"Vlastní URL pro Element Call"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Nastavte vlastní URL pro Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Neplatné URL, ujistěte se, že jste uvedli protokol (http/https) a správnou adresu."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Skrýt avatary v žádostech o pozvání do místnosti"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Skrýt náhledy médií na časové ose"</string>
<string name="screen_advanced_settings_labs">"Experimentální funkce"</string>
<string name="screen_advanced_settings_media_compression_description">"Rychlejší nahrávání fotografií a videí a snížení spotřeby dat"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimalizace kvality médií"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderování a bezpečnost"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Automaticky optimalizovat obrázky pro rychlejší nahrávání a menší velikosti souborů."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimalizace kvality nahrávání obrázků"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Klepnutím sem provedete změnu."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Vysoká (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Nízká (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Standardní (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Kvalita nahrávání videa"</string>
<string name="screen_advanced_settings_push_provider_android">"Poskytovatel push oznámení"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Vypněte editor formátovaného textu pro ruční zadání Markdown."</string>
<string name="screen_advanced_settings_send_read_receipts">"Potvrzení o přečtení"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Pokud je vypnuto, potvrzení o přečtení se nikomu neodesílají. Stále budete dostávat potvrzení o přečtení od ostatních uživatelů."</string>
<string name="screen_advanced_settings_share_presence">"Sdílejte přítomnost"</string>
<string name="screen_advanced_settings_share_presence_description">"Pokud je tato funkce vypnutá, nebudete moci odesílat ani přijímat potvrzení o přečtení ani upozornění o psaní."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Vždy skrýt"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Vždy zobrazit"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"V soukromých místnostech"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Skryté médium lze vždy zobrazit klepnutím na něj"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Zobrazit média na časové ose"</string>
<string name="screen_advanced_settings_view_source_description">"Povolit možnost zobrazení zdroje zprávy na časové ose."</string>
<string name="screen_blocked_users_empty">"Nemáte žádné blokované uživatele"</string>
<string name="screen_blocked_users_unblock_alert_action">"Odblokovat"</string>
<string name="screen_blocked_users_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
<string name="screen_blocked_users_unblock_alert_title">"Odblokovat uživatele"</string>
<string name="screen_blocked_users_unblocking">"Odblokování…"</string>
<string name="screen_edit_profile_display_name">"Zobrazované jméno"</string>
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované jméno"</string>
<string name="screen_edit_profile_error">"Došlo k neznámé chybě a informace nelze změnit."</string>
<string name="screen_edit_profile_error_title">"Nelze aktualizovat profil"</string>
<string name="screen_edit_profile_title">"Upravit profil"</string>
<string name="screen_edit_profile_updating_details">"Aktualizace profilu…"</string>
<string name="screen_labs_enable_threads">"Povolit odpovědi ve vlákně"</string>
<string name="screen_labs_enable_threads_description">"Aplikace se restartuje, aby se tato změna projevila."</string>
<string name="screen_labs_header_description">"Vyzkoušejte naše nejnovější nápady, které jsou ve vývoji. Tyto funkce nejsou finalizované; mohou být nestabilní a mohou se změnit."</string>
<string name="screen_labs_header_title">"Máte chuť experimentovat?"</string>
<string name="screen_labs_title">"Experimentální funkce"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Další nastavení"</string>
<string name="screen_notification_settings_calls_label">"Halsové a video hovory"</string>
<string name="screen_notification_settings_configuration_mismatch">"Neshoda konfigurace"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností.
Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní.
Pokud budete pokračovat, některá nastavení se mohou změnit."</string>
<string name="screen_notification_settings_direct_chats">"Přímé zprávy"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Vlastní nastavení pro chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Při aktualizaci nastavení oznámení došlo k chybě."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Všechny zprávy"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Pouze zmínky a klíčová slova"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"V přímých zprávách mě upozornit na"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Ve skupinových chatech mě upozornit na"</string>
<string name="screen_notification_settings_enable_notifications">"Povolit oznámení na tomto zařízení"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfigurace nebyla opravena, zkuste to prosím znovu."</string>
<string name="screen_notification_settings_group_chats">"Skupinové chaty"</string>
<string name="screen_notification_settings_invite_for_me_label">"Pozvánky"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server tuto možnost v zašifrovaných místnostech nepodporuje, v některých místnostech nemusíte být upozorněni."</string>
<string name="screen_notification_settings_mentions_section_title">"Zmínky"</string>
<string name="screen_notification_settings_mode_all">"Vše"</string>
<string name="screen_notification_settings_mode_mentions">"Zmínky"</string>
<string name="screen_notification_settings_notification_section_title">"Upozornit mě na"</string>
<string name="screen_notification_settings_room_mention_label">"Upozornit mě na @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Chcete-li dostávat oznámení, změňte prosím svůj %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systémová nastavení"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systémová oznámení byla vypnuta"</string>
<string name="screen_notification_settings_title">"Oznámení"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Historie push oznámení"</string>
<string name="troubleshoot_notifications_entry_point_section">"Odstraňování problémů"</string>
<string name="troubleshoot_notifications_entry_point_title">"Odstraňování problémů s upozorněními"</string>
</resources>
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Dewiswch sut i dderbyn hysbysiadau"</string>
<string name="screen_advanced_settings_developer_mode">"Modd datblygwr"</string>
<string name="screen_advanced_settings_developer_mode_description">"Galluogi i gael mynediad at nodweddion a swyddogaethau datblygwyr."</string>
<string name="screen_advanced_settings_element_call_base_url">"URL sylfaen Galwad Element Cyfaddas"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Gosod URL sylfaen cyfaddas ar gyfer Galwad Element."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL annilys, gwnewch yn siŵr eich bod yn cynnwys y protocol (http/https) a\'r cyfeiriad cywir."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Cuddio afatarau yn y ceisiadau gwahoddiad i ystafell"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Cuddio rhagolygon cyfryngau yn y llinell amser"</string>
<string name="screen_advanced_settings_media_compression_description">"Llwythwch i fyny lluniau a fideos yn gynt a lleihau\'r defnydd o ddata"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimeiddio ansawdd y cyfryngau"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Cymedroli a Diogelwch"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimeiddio delweddau\'n awtomatig ar gyfer llwytho cyflymach a meintiau ffeiliau llai."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimeiddio ansawdd llwytho delweddau"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s Tapiwch yma i newid."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Uchel (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Isel (480c)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Safonol (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Ansawdd lwytho fideo"</string>
<string name="screen_advanced_settings_push_provider_android">"Darparwr hysbysiad gwthio"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Analluogi\'r golygydd testun cyfoethog i deipio Markdown â llaw."</string>
<string name="screen_advanced_settings_send_read_receipts">"Derbynebau darllen"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Os wedi\'i ddiffodd, fydd eich derbynebau darllen ddim yn cael eu hanfon at unrhyw un. Byddwch yn dal i dderbyn derbynebau darllen gan ddefnyddwyr eraill."</string>
<string name="screen_advanced_settings_share_presence">"Rhannu presenoldeb"</string>
<string name="screen_advanced_settings_share_presence_description">"Os wedi\'i ddiffodd, fyddwch chi ddim yn gallu anfon na derbyn derbynebau wedi\'u darllen na hysbysiadau teipio."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Cuddio bob tro"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Dangos bob tro"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Mewn ystafelloedd preifat"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Mae modd dangos cyfrwng cudd trwy dapio arno"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Dangos cyfryngau mewn llinell amser"</string>
<string name="screen_advanced_settings_view_source_description">"Galluogi\'r dewis i weld ffynhonnell y neges yn y llinell amser."</string>
<string name="screen_blocked_users_empty">"Does gennych chi ddim defnyddwyr sydd wedi\'u rhwystro"</string>
<string name="screen_blocked_users_unblock_alert_action">"Dad-rwystro"</string>
<string name="screen_blocked_users_unblock_alert_description">"Byddwch yn gallu gweld pob neges oddi wrthyn nhw eto."</string>
<string name="screen_blocked_users_unblock_alert_title">"Dadrwystro defnyddiwr"</string>
<string name="screen_blocked_users_unblocking">"Wrthi\'n dadrwystro…"</string>
<string name="screen_edit_profile_display_name">"Enw dangos"</string>
<string name="screen_edit_profile_display_name_placeholder">"Eich enw dangos"</string>
<string name="screen_edit_profile_error">"Cafwyd gwall anhysbys a doedd dim modd newid y manylion."</string>
<string name="screen_edit_profile_error_title">"Methu diweddaru\'r proffil"</string>
<string name="screen_edit_profile_title">"Golygu proffil"</string>
<string name="screen_edit_profile_updating_details">"Yn diweddaru proffil…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Gosodiadau ychwanegol"</string>
<string name="screen_notification_settings_calls_label">"Galwadau sain a fideo"</string>
<string name="screen_notification_settings_configuration_mismatch">"Anghydweddiad y ffurfweddiad"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Rydym wedi symleiddio\'r Gosodiadau Hysbysiadau i\'w gwneud yn haws dod o hyd i ddewisiadau. Nid yw rhai gosodiadau cyfaddas rydych chi wedi\'u dewis yn y gorffennol yn cael eu dangos yma, ond maen nhw\'n dal yn weithredol.
Os ewch ymlaen, efallai y bydd rhai o\'ch gosodiadau\'n newid."</string>
<string name="screen_notification_settings_direct_chats">"Sgyrsiau uniongyrchol"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Gosodiad cyfaddas fesul sgwrs"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Digwyddodd gwall wrth ddiweddaru\'r gosodiad hysbysu."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Pob neges"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Crybwylliadau ac Allweddeiriau\'n unig"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Ar sgyrsiau uniongyrchol, rhoi gwybod i mi am"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Ar sgyrsiau grŵp, rhoi gwybod i mi am"</string>
<string name="screen_notification_settings_enable_notifications">"Galluogi hysbysiadau ar y ddyfais hon"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Nid yw\'r ffurfweddiad wedi\'i gywiro, ceisiwch eto."</string>
<string name="screen_notification_settings_group_chats">"Sgyrsiau grŵp"</string>
<string name="screen_notification_settings_invite_for_me_label">"Gwahoddiadau"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Nid yw eich gweinydd cartref yn cefnogi\'r dewis hwn mewn ystafelloedd sydd wedi\'u hamgryptio, efallai fyddwch chi ddim yn cael gwybod mewn rhai ystafelloedd."</string>
<string name="screen_notification_settings_mentions_section_title">"Crybwyll"</string>
<string name="screen_notification_settings_mode_all">"Y Cyfan"</string>
<string name="screen_notification_settings_mode_mentions">"Crybwyll"</string>
<string name="screen_notification_settings_notification_section_title">"Rhoi gwybod i mi am"</string>
<string name="screen_notification_settings_room_mention_label">"Rhoi gwybod i mi ar @ystafell"</string>
<string name="screen_notification_settings_system_notifications_action_required">"I dderbyn hysbysiadau, newidiwch eich %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"gosodiadau system"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Hysbysiadau system wedi\'u diffodd"</string>
<string name="screen_notification_settings_title">"Hysbysiadau"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Hanes gwthio"</string>
<string name="troubleshoot_notifications_entry_point_section">"Datrys Problemau"</string>
<string name="troubleshoot_notifications_entry_point_title">"Hysbysiadau datrys problemau"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Vælg, hvordan du vil modtage notifikationer"</string>
<string name="screen_advanced_settings_developer_mode">"Udviklertilstand"</string>
<string name="screen_advanced_settings_developer_mode_description">"Aktivér for at få adgang til funktioner og funktionalitet for udviklere."</string>
<string name="screen_advanced_settings_element_call_base_url">"Brugerdefineret URL til opkaldsbase for Element"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Angiv en brugerdefineret basis-URL til Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Ugyldig URL, sørg for at inkludere protokollen (http/https) og den korrekte adresse."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Skjul avatarer i anmodninger om invitation til rum"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Skjul forhåndsvisning af medier i tidslinjen"</string>
<string name="screen_advanced_settings_labs">"Laboratorier"</string>
<string name="screen_advanced_settings_media_compression_description">"Upload fotos og videoer hurtigere, og reducér dataforbrug"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimér mediekvaliteten"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderation og sikkerhed"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimér automatisk billeder for hurtigere uploads og mindre filstørrelser."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimér kvaliteten på overførte billeder"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Tryk her for at ændre."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Høj (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Lav (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Standard (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Kvalitet på overførte videoer"</string>
<string name="screen_advanced_settings_push_provider_android">"Udbyder af push-notifikationer"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Deaktiver rich text-editoren for at skrive Markdown manuelt."</string>
<string name="screen_advanced_settings_send_read_receipts">"Kvitteringer​•​for​•​læsning"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Hvis deaktiveret, sendes dine læsekvitteringer ikke til nogen. Du vil stadig modtage læsekvitteringer fra andre brugere."</string>
<string name="screen_advanced_settings_share_presence">"Del tilstedeværelse"</string>
<string name="screen_advanced_settings_share_presence_description">"Hvis deaktiveret, kan du ikke sende eller modtage læsekvitteringer eller indtastningsmeddelelser."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Skjul altid"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Vis altid"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"I private rum"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Et skjult medie kan altid vises ved at trykke på det"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Vis medier i tidslinjen"</string>
<string name="screen_advanced_settings_view_source_description">"Aktivér mulighed for at se meddelelseskilden i tidslinjen."</string>
<string name="screen_blocked_users_empty">"Du har ingen blokerede brugere"</string>
<string name="screen_blocked_users_unblock_alert_action">"Fjern blokering"</string>
<string name="screen_blocked_users_unblock_alert_description">"Du vil være i stand til at se alle beskeder fra dem igen."</string>
<string name="screen_blocked_users_unblock_alert_title">"Fjern blokering af bruger"</string>
<string name="screen_blocked_users_unblocking">"Fjerner blokering…"</string>
<string name="screen_edit_profile_display_name">"Vist navn"</string>
<string name="screen_edit_profile_display_name_placeholder">"Dit viste navn"</string>
<string name="screen_edit_profile_error">"Der opstod en ukendt fejl, og oplysningerne kunne ikke ændres."</string>
<string name="screen_edit_profile_error_title">"Kan ikke opdatere profilen"</string>
<string name="screen_edit_profile_title">"Redigér profil"</string>
<string name="screen_edit_profile_updating_details">"Opdaterer profil…"</string>
<string name="screen_labs_enable_threads">"Aktivér svar-tråde"</string>
<string name="screen_labs_enable_threads_description">"Appen genstarter for at anvende denne ændring."</string>
<string name="screen_labs_header_description">"Prøv vores nyeste idéer under udvikling. Disse funktioner er ikke færdige; de kan være ustabile og kan ændre sig."</string>
<string name="screen_labs_header_title">"Er du i humør til at eksperimentere?"</string>
<string name="screen_labs_title">"Laboratorier"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Yderligere indstillinger"</string>
<string name="screen_notification_settings_calls_label">"Lyd- og videoopkald"</string>
<string name="screen_notification_settings_configuration_mismatch">"Uoverensstemmelse i konfigurationen"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Vi har forenklet indstillingerne for notifikationer for at gøre det nemmere at finde dem. Nogle af de brugerdefinerede indstillinger, du tidligere har valgt, vises ikke her, men de er stadig aktive.
Hvis du fortsætter, kan nogle af dine indstillinger blive ændret."</string>
<string name="screen_notification_settings_direct_chats">"Direkte samtaler"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Brugerdefineret indstilling pr. samtale"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Der opstod en fejl under opdatering af notifikationsindstillingen."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Alle beskeder"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Kun omtaler og nøgleord"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"I direkte samtaler, giv mig besked om"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"I gruppesamtaler, giv mig besked ved"</string>
<string name="screen_notification_settings_enable_notifications">"Aktivér notifikationer på denne enhed"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfigurationen er ikke blevet rettet, prøv igen."</string>
<string name="screen_notification_settings_group_chats">"Gruppesamtaler"</string>
<string name="screen_notification_settings_invite_for_me_label">"Invitationer"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Din hjemmeserver understøtter ikke denne mulighed i krypterede rum, og derfor er det muligt at du ikke får besked i alle rum."</string>
<string name="screen_notification_settings_mentions_section_title">"Omtaler"</string>
<string name="screen_notification_settings_mode_all">"Alle"</string>
<string name="screen_notification_settings_mode_mentions">"Omtaler"</string>
<string name="screen_notification_settings_notification_section_title">"Giv mig besked om"</string>
<string name="screen_notification_settings_room_mention_label">"Giv mig besked ved @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"For at modtage notifikationer, skal du ændre din %1$s ."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systemindstillinger"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systemmeddelelser slået fra"</string>
<string name="screen_notification_settings_title">"Notifikationer"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Push-historik"</string>
<string name="troubleshoot_notifications_entry_point_section">"Fejlfind"</string>
<string name="troubleshoot_notifications_entry_point_title">"Fejlfinding af meddelelser"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Wähle aus, wie du Benachrichtigungen erhalten möchtest"</string>
<string name="screen_advanced_settings_developer_mode">"Entwicklermodus"</string>
<string name="screen_advanced_settings_developer_mode_description">"Aktivieren, um Zugriff auf Features und Funktionen für Entwickler zu aktivieren."</string>
<string name="screen_advanced_settings_element_call_base_url">"Benutzerdefinierte Element Call Basis-URL"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Lege eine eigene Basis-URL für Element Call fest."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Ungültige URL, bitte gib das Protokoll (http/https) und die richtige Adresse an."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Avatare in Chateinladungen ausblenden"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Medienvorschau im Nachrichtenverlauf ausblenden"</string>
<string name="screen_advanced_settings_labs">"Labs"</string>
<string name="screen_advanced_settings_media_compression_description">"Lade Fotos und Videos schneller hoch und reduziere den Datenverbrauch"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimiere die Medienqualität"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderation und Sicherheit"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimiere Bilder automatisch für schnellere Uploads und kleinere Dateigrößen."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimiere die Qualität zum Hochladen von Bildern."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Tippe hier, um zu ändern."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Hoch (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Niedrig (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Standard (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Video-Upload-Qualität"</string>
<string name="screen_advanced_settings_push_provider_android">"Dienst für Push-Benachrichtigungen"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Deaktiviere den Rich-Text-Editor, um Markdown manuell einzugeben."</string>
<string name="screen_advanced_settings_send_read_receipts">"Lesebestätigungen"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Wenn diese Option deaktiviert ist, werden deine Lesebestätigungen an niemanden gesendet. Du erhältst weiterhin Lesebestätigungen von anderen Nutzern."</string>
<string name="screen_advanced_settings_share_presence">"Präsenz teilen"</string>
<string name="screen_advanced_settings_share_presence_description">"Wenn diese Option deaktiviert ist, kannst du keine Lesebestätigungen oder Tipp-Indikatoren senden oder empfangen."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Immer ausblenden"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Immer anzeigen"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"In privaten Chats"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Ausgeblendete Medien können jederzeit durch Antippen angezeigt werden"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Medien im Nachrichtenverlauf anzeigen"</string>
<string name="screen_advanced_settings_view_source_description">"Aktiviere die Option, um die Quelle der Nachricht im Nachrichtenverlauf zu sehen."</string>
<string name="screen_blocked_users_empty">"Du hast keine blockierten Nutzer"</string>
<string name="screen_blocked_users_unblock_alert_action">"Blockierung aufheben"</string>
<string name="screen_blocked_users_unblock_alert_description">"Der Nutzer kann dir wieder Nachrichten senden &amp; alle Nachrichten des Nutzers werden wieder angezeigt."</string>
<string name="screen_blocked_users_unblock_alert_title">"Blockierung aufheben"</string>
<string name="screen_blocked_users_unblocking">"Blockierung wird aufgehoben…"</string>
<string name="screen_edit_profile_display_name">"Anzeigename"</string>
<string name="screen_edit_profile_display_name_placeholder">"Dein Anzeigename"</string>
<string name="screen_edit_profile_error">"Ein unbekannter Fehler ist aufgetreten und die Informationen konnten nicht geändert werden."</string>
<string name="screen_edit_profile_error_title">"Profil kann nicht aktualisiert werden"</string>
<string name="screen_edit_profile_title">"Profil bearbeiten"</string>
<string name="screen_edit_profile_updating_details">"Profil wird aktualisiert…"</string>
<string name="screen_labs_enable_threads">"Thread-Antworten aktivieren"</string>
<string name="screen_labs_enable_threads_description">"Die App wird neu gestartet, um diese Änderung zu übernehmen."</string>
<string name="screen_labs_header_description">"Probier unsere neuesten Ideen in der Entwicklung aus. Diese Funktionen sind noch nicht fertiggestellt; sie können instabil sein und sich noch ändern."</string>
<string name="screen_labs_header_title">"Entdeckungsfreudig?"</string>
<string name="screen_labs_title">"Labs"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Zusätzliche Einstellungen"</string>
<string name="screen_notification_settings_calls_label">"Audio- und Videoanrufe"</string>
<string name="screen_notification_settings_configuration_mismatch">"Konfiguration stimmt nicht überein"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Wir haben die Einstellungen für Benachrichtigungen vereinfacht. Einige Einstellungen, die du gewählt hast, werden hier nicht angezeigt, sind aber immer noch aktiv.
Wenn du fortfährst, können sich einige deiner Einstellungen ändern."</string>
<string name="screen_notification_settings_direct_chats">"Direktnachrichten"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Benutzerdefinierte Einstellung pro Chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Alle Nachrichten"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Benachrichtige mich bei Direktnachrichten über"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Bei Gruppenchats benachrichtige mich bei"</string>
<string name="screen_notification_settings_enable_notifications">"Benachrichtigungen auf diesem Gerät aktivieren"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Die Konfiguration wurde nicht korrigiert, bitte versuche es erneut."</string>
<string name="screen_notification_settings_group_chats">"Gruppenchats"</string>
<string name="screen_notification_settings_invite_for_me_label">"Einladungen"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Dein Homeserver unterstützt diese Option in verschlüsselten Chats nicht. In einigen Chats erhältst du möglicherweise keine Benachrichtigungen."</string>
<string name="screen_notification_settings_mentions_section_title">"Erwähnungen"</string>
<string name="screen_notification_settings_mode_all">"Alle"</string>
<string name="screen_notification_settings_mode_mentions">"Erwähnungen"</string>
<string name="screen_notification_settings_notification_section_title">"Benachrichtige mich bei"</string>
<string name="screen_notification_settings_room_mention_label">"Benachrichtige mich bei @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Um Benachrichtigungen zu erhalten, ändere bitte deine %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"Systemeinstellungen"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systembenachrichtigungen deaktiviert"</string>
<string name="screen_notification_settings_title">"Benachrichtigungen"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Verlauf pushen"</string>
<string name="troubleshoot_notifications_entry_point_section">"Fehlerbehebung"</string>
<string name="troubleshoot_notifications_entry_point_title">"Fehlerbehebung für Benachrichtigungen"</string>
</resources>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"Για να διασφαλίσετε ότι δεν θα χάσετε ποτέ μια σημαντική κλήση, αλλάξτε τις ρυθμίσεις σας ώστε να επιτρέπονται οι ειδοποιήσεις πλήρους οθόνης όταν το τηλέφωνό σας είναι κλειδωμένο."</string>
<string name="full_screen_intent_banner_title">"Βελτίωσε την εμπειρία κλήσεων"</string>
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Επέλεξε τον τρόπο λήψης ειδοποιήσεων"</string>
<string name="screen_advanced_settings_developer_mode">"Λειτουργία προγραμματιστή"</string>
<string name="screen_advanced_settings_developer_mode_description">"Ενεργοποίησε την πρόσβαση σε δυνατότητες και λειτουργικότητα για προγραμματιστές."</string>
<string name="screen_advanced_settings_element_call_base_url">"Προσαρμοσμένο URL βάσης κλήσεων Element"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Όρισε μια προσαρμοσμένη διεύθυνση βάσης URL για κλήση Element."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Μη έγκυρη διεύθυνση URL, βεβαιώσου ότι έχεις συμπεριλάβει το πρωτόκολλο (http/https) και τη σωστή διεύθυνση."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Απόκρυψη εικόνων προφίλ σε αιτήματα πρόσκλησης αίθουσας"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Απόκρυψη προεπισκοπήσεων πολυμέσων στο timeline"</string>
<string name="screen_advanced_settings_media_compression_description">"Ανέβασε φωτογραφίες και βίντεο γρηγορότερα και μείωσε τη χρήση δεδομένων"</string>
<string name="screen_advanced_settings_media_compression_title">"Βελτιστοποίηση ποιότητας των μέσων"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Συντονισμός και Ασφάλεια"</string>
<string name="screen_advanced_settings_push_provider_android">"Πάροχος ειδοποιήσεων push"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Απενεργοποίησε τον επεξεργαστή εμπλουτισμένου κειμένου για να πληκτρολογήσεις Markdown χειροκίνητα."</string>
<string name="screen_advanced_settings_send_read_receipts">"Αποδεικτικά ανάγνωσης"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Εάν απενεργοποιηθεί, τα αποδεικτικά ανάγνωσης δεν θα στέλνονται σε κανέναν. Θα εξακολουθείς να λαμβάνεις αποδεικτικά ανάγνωσης από άλλους χρήστες."</string>
<string name="screen_advanced_settings_share_presence">"Κοινή χρήση παρουσίας"</string>
<string name="screen_advanced_settings_share_presence_description">"Εάν απενεργοποιηθεί, δεν θα μπορείς να στέλνεις ή να λαμβάνεις αποδεικτικά ανάγνωσης ή ειδοποιήσεις πληκτρολόγησης."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Πάντα απόκρυψη"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Πάντα εμφάνιση"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Σε ιδιωτικές αίθουσες"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Ένα κρυφό πολυμέσο μπορεί πάντα να εμφανιστεί πατώντας το"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Εμφάνιση πολυμέσων στο timeline"</string>
<string name="screen_advanced_settings_view_source_description">"Ενεργοποίησε την επιλογή για προβολή πηγής μηνυμάτων στη ροή."</string>
<string name="screen_blocked_users_empty">"Δεν έχεις αποκλεισμένους χρήστες"</string>
<string name="screen_blocked_users_unblock_alert_action">"Άρση αποκλεισμού"</string>
<string name="screen_blocked_users_unblock_alert_description">"Θα μπορείς να δεις ξανά όλα τα μηνύματα του."</string>
<string name="screen_blocked_users_unblock_alert_title">"Κατάργηση αποκλεισμού χρήστη"</string>
<string name="screen_blocked_users_unblocking">"Άρση αποκλεισμού…"</string>
<string name="screen_edit_profile_display_name">"Εμφανιζόμενο όνομα"</string>
<string name="screen_edit_profile_display_name_placeholder">"Το εμφανιζόμενο όνομά σου"</string>
<string name="screen_edit_profile_error">"Παρουσιάστηκε ένα άγνωστο σφάλμα και οι πληροφορίες δεν μπορούσαν να αλλάξουν."</string>
<string name="screen_edit_profile_error_title">"Δεν είναι δυνατή η ενημέρωση του προφίλ"</string>
<string name="screen_edit_profile_title">"Επεξεργασία προφίλ"</string>
<string name="screen_edit_profile_updating_details">"Ενημέρωση προφίλ…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Πρόσθετες ρυθμίσεις"</string>
<string name="screen_notification_settings_calls_label">"Κλήσεις ήχου και βίντεο"</string>
<string name="screen_notification_settings_configuration_mismatch">"Αναντιστοιχία διαμόρφωσης"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Απλοποιήσαμε τις Ρυθμίσεις Ειδοποιήσεων για να διευκολύνουμε την εύρεση επιλογών. Ορισμένες προσαρμοσμένες ρυθμίσεις που έχετε επιλέξει στο παρελθόν δεν εμφανίζονται εδώ, αλλά εξακολουθούν να είναι ενεργές.
Εάν προχωρήσεις, ορισμένες από τις ρυθμίσεις σας ενδέχεται να αλλάξουν."</string>
<string name="screen_notification_settings_direct_chats">"Άμεσες συνομιλίες"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Προσαρμοσμένη ρύθμιση ανά συνομιλία"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Παρουσιάστηκε σφάλμα κατά την ενημέρωση της ρύθμισης ειδοποίησης."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Όλα τα μηνύματα"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Μόνο αναφορές και λέξεις-κλειδιά"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Σε άμεσες συνομιλίες, ειδοποίησέ με για"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Σε ομαδικές συνομιλίες, ειδοποίησέ με για"</string>
<string name="screen_notification_settings_enable_notifications">"Ενεργοποίηση ειδοποιήσεων σε αυτήν τη συσκευή"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Η διαμόρφωση δεν έχει διορθωθεί, δοκίμασε ξανά."</string>
<string name="screen_notification_settings_group_chats">"Ομαδικές συνομιλίες"</string>
<string name="screen_notification_settings_invite_for_me_label">"Προσκλήσεις"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ο αρχικός διακομιστής σας δεν υποστηρίζει αυτή την επιλογή σε κρυπτογραφημένες αίθουσες, ενδέχεται να μην λαμβάνετε ειδοποιήσεις σε ορισμένες αίθουσες."</string>
<string name="screen_notification_settings_mentions_section_title">"Αναφορές"</string>
<string name="screen_notification_settings_mode_all">"Όλα"</string>
<string name="screen_notification_settings_mode_mentions">"Αναφορές"</string>
<string name="screen_notification_settings_notification_section_title">"Ειδοποίησέ με για"</string>
<string name="screen_notification_settings_room_mention_label">"Ειδοποίηση για @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Για να λαμβάνεις ειδοποιήσεις, άλλαξε το %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"ρυθμίσεις συστήματος"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Ειδοποιήσεις συστήματος ανενεργές"</string>
<string name="screen_notification_settings_title">"Ειδοποιήσεις"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Ιστορικό push"</string>
<string name="troubleshoot_notifications_entry_point_section">"Αντιμετώπιση προβλημάτων"</string>
<string name="troubleshoot_notifications_entry_point_title">"Αντιμετώπιση προβλημάτων ειδοποιήσεων"</string>
</resources>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_media_compression_title">"Optimize media quality"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Automatically optimize images for faster uploads and smaller file sizes."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimize image upload quality"</string>
</resources>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Elige cómo recibir las notificaciones"</string>
<string name="screen_advanced_settings_developer_mode">"Modo desarrollador"</string>
<string name="screen_advanced_settings_developer_mode_description">"Habilita para tener acceso a características y funcionalidades para desarrolladores."</string>
<string name="screen_advanced_settings_element_call_base_url">"URL base personalizada de Element Call"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Define una URL base personalizada para Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL no válida, asegúrate de incluir el protocolo (http/https) y la dirección correcta."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Ocultar avatares en las invitaciones a salas"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Ocultar vistas previas de multimedia en la cronología"</string>
<string name="screen_advanced_settings_media_compression_description">"Sube fotos y vídeos más rápido y reduce el uso de datos"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimizar la calidad de los medios"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderación y seguridad"</string>
<string name="screen_advanced_settings_push_provider_android">"Proveedor de notificaciones push"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Desactiva el editor de texto enriquecido para escribir Markdown manualmente."</string>
<string name="screen_advanced_settings_send_read_receipts">"Confirmaciones de lectura"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Si se desactiva, las confirmaciones de lectura no se enviarán a nadie. Seguirás recibiendo confirmaciones de lectura de otros usuarios."</string>
<string name="screen_advanced_settings_share_presence">"Compartir presencia"</string>
<string name="screen_advanced_settings_share_presence_description">"Si se desactiva, no podrás enviar ni recibir confirmaciones de lectura ni notificaciones de escritura."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Ocultar siempre"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Mostrar siempre"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"En las salas privadas"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Siempre se puede mostrar un ítem multimedia oculto pulsando sobre él"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Mostrar multimedia en la cronología"</string>
<string name="screen_advanced_settings_view_source_description">"Habilita la opción para ver el contenido en bruto del mensaje en la cronología."</string>
<string name="screen_blocked_users_empty">"No tienes usuarios bloqueados"</string>
<string name="screen_blocked_users_unblock_alert_action">"Desbloquear"</string>
<string name="screen_blocked_users_unblock_alert_description">"Podrás ver todos sus mensajes de nuevo."</string>
<string name="screen_blocked_users_unblock_alert_title">"Desbloquear usuario"</string>
<string name="screen_blocked_users_unblocking">"Desbloqueando…"</string>
<string name="screen_edit_profile_display_name">"Nombre público"</string>
<string name="screen_edit_profile_display_name_placeholder">"Tu nombre visible"</string>
<string name="screen_edit_profile_error">"Se encontró un error desconocido y no se pudo cambiar la información."</string>
<string name="screen_edit_profile_error_title">"No se puede actualizar el perfil"</string>
<string name="screen_edit_profile_title">"Editar perfil"</string>
<string name="screen_edit_profile_updating_details">"Actualizando perfil…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Ajustes adicionales"</string>
<string name="screen_notification_settings_calls_label">"Llamadas de audio y vídeo"</string>
<string name="screen_notification_settings_configuration_mismatch">"La configuración no coincide"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Hemos simplificado la Configuración de Notificaciones para hacer las opciones más fáciles de encontrar. Algunas configuraciones personalizadas que has elegido en el pasado no se muestran aquí, pero siguen activas.
Si continúas, es posible que algunos de tus ajustes cambien."</string>
<string name="screen_notification_settings_direct_chats">"Chats directos"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Configuración personalizada por chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Se ha producido un error al actualizar la configuración de notificaciones."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Todos los mensajes"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Únicamente Menciones y Palabras clave"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"En los chats directos, notifícame por"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"En los chats grupales, notifícame por"</string>
<string name="screen_notification_settings_enable_notifications">"Habilitar las notificaciones en este dispositivo"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"La configuración no se ha corregido, por favor inténtalo de nuevo."</string>
<string name="screen_notification_settings_group_chats">"Chats grupales"</string>
<string name="screen_notification_settings_invite_for_me_label">"Invitaciones"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Tu servidor base no admite esta opción en salas cifradas, puede que no recibas notificaciones de algunas salas."</string>
<string name="screen_notification_settings_mentions_section_title">"Menciones"</string>
<string name="screen_notification_settings_mode_all">"Todos"</string>
<string name="screen_notification_settings_mode_mentions">"Menciones"</string>
<string name="screen_notification_settings_notification_section_title">"Notificarme para"</string>
<string name="screen_notification_settings_room_mention_label">"Notificarme con @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Para recibir notificaciones, cambia tus %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"ajustes del sistema"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Notificaciones del sistema desactivadas"</string>
<string name="screen_notification_settings_title">"Notificaciones"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Historial de notificaciones push"</string>
<string name="troubleshoot_notifications_entry_point_section">"Solucionar problemas"</string>
<string name="troubleshoot_notifications_entry_point_title">"Solucionar problemas con las notificaciones"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Vali kuidas sa soovid saada teavitusi"</string>
<string name="screen_advanced_settings_developer_mode">"Arendaja valikud"</string>
<string name="screen_advanced_settings_developer_mode_description">"Selle eelistuse sisselülitamisel lisanduvad rakendusse arendaja tööks vajalikud valikud."</string>
<string name="screen_advanced_settings_element_call_base_url">"Element Calli kohandatud teenuseaadress"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Seadista kohandatud teenuseaadress Element Calli jaoks."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Vigane url. Palun vaata, et url algaks protokolliga (http/https) ning aadress ise oleks ka õige."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Peida jututubade kutsetest tunnuspildid"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Peida meedia eelvaated ajajoonel"</string>
<string name="screen_advanced_settings_labs">"Katsed"</string>
<string name="screen_advanced_settings_media_compression_description">"Sellega laadid fotosid ja videoid kiiremini üles ning vähendad andmemahtu"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimeeri meedia kvaliteeti"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Modereerimine ja ohutus"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Kiirema üleslaadimise ja väiksemate failide nimel optimeeri pilte automaatselt."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimeeri üleslaaditavate piltide kvaliteeti."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Muutmiseks klõpsi siin."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Kõrge (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Madal (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Standard (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Üleslaaditavate videote kvaliteet"</string>
<string name="screen_advanced_settings_push_provider_android">"Tõuketeavituste pakkuja"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Kui soovid Markdown-vormingut käsitsi lisada, siis lülita vormindatud teksti toimeti välja."</string>
<string name="screen_advanced_settings_send_read_receipts">"Lugemisteatised"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Kui lülitad selle valiku välja, siis mitte keegi enam ei saa sinult lugemisteatisi. Küll aga saad sina teiste kasutajate lugemisteatisi."</string>
<string name="screen_advanced_settings_share_presence">"Jaga oma olekut"</string>
<string name="screen_advanced_settings_share_presence_description">"Kui see eelistus on välja lülitatud, siis sa ei saa ega saada ei lugemisteatisi ega kirjutamise teavitusi."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Peida alati"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Näita alati"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Privaatsetes jututubades"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Peidetud meediumi saad alati näha temal klõpsides"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Näita ajajoonel meediat"</string>
<string name="screen_advanced_settings_view_source_description">"Selle eelistuse sisselülitamisel on võimalik ajajoonel vaadata sõnumite lähtekoodi."</string>
<string name="screen_blocked_users_empty">"Sa pole ühtegi kasutajat blokeerinud"</string>
<string name="screen_blocked_users_unblock_alert_action">"Eemalda blokeering"</string>
<string name="screen_blocked_users_unblock_alert_description">"Nüüd näed sa jälle kõiki tema sõnumeid"</string>
<string name="screen_blocked_users_unblock_alert_title">"Eemalda kasutajalt blokeering"</string>
<string name="screen_blocked_users_unblocking">"Eemaldame blokeeringu…"</string>
<string name="screen_edit_profile_display_name">"Kuvatav nimi"</string>
<string name="screen_edit_profile_display_name_placeholder">"Sinu kuvatav nimi"</string>
<string name="screen_edit_profile_error">"Tekkis tundmatu viga ning teavet ei õnnestunud muuta."</string>
<string name="screen_edit_profile_error_title">"Profiili uuendamine ei õnnestunud"</string>
<string name="screen_edit_profile_title">"Muuda profiili"</string>
<string name="screen_edit_profile_updating_details">"Profiil on muutmisel…"</string>
<string name="screen_labs_enable_threads">"Võta kasutusele vastamine jutulõngas"</string>
<string name="screen_labs_enable_threads_description">"Selle muudatuse jõustamiseks käivitub rakendus uuesti."</string>
<string name="screen_labs_header_description">"Katseta meie uusimaid arendusideid. Need funktsionaalsused pole veel lõplikud, nad ei pruugi toimida parimal viisil ning võivad veel muutuda."</string>
<string name="screen_labs_header_title">"Kas tahad katsetada?"</string>
<string name="screen_labs_title">"Katsed"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Täiendavad seadistused"</string>
<string name="screen_notification_settings_calls_label">"Hääl- ja videokõned"</string>
<string name="screen_notification_settings_configuration_mismatch">"Eelistused ei sobi omavahel"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Et eelistusi oleks kergem leida, me oleme lihtsustanud teavituste seadistusi. Kuigi mõned varem valitud eelistused pole siin näha, siis nad kehtivad jätkuvalt.
Kui sa jätkad muutmist, siis võivad muutuda ka need peidetud eelistused."</string>
<string name="screen_notification_settings_direct_chats">"Otsevestlused"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Kohandatud seadistused eraldi igale vestlusele"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Teavituste seadistamisel tekkis viga"</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Kõikide sõnumite korral"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Mainimiste ja võtmesõnade alusel"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Otsevestluste puhul teavita mind"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Jututubades teavita mind"</string>
<string name="screen_notification_settings_enable_notifications">"Lülita teavitused selles seadmes sisse"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Seadistus on veel parandamata, palun proovi uuesti."</string>
<string name="screen_notification_settings_group_chats">"Rühmavestlused"</string>
<string name="screen_notification_settings_invite_for_me_label">"Kutsed"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Sinu koduserver ei toeta seda funktsionaalsust krüptitud jututubades ja seega ei pruugi kõik teavitused sinuni jõuda."</string>
<string name="screen_notification_settings_mentions_section_title">"Mainimiste alusel"</string>
<string name="screen_notification_settings_mode_all">"Kõik"</string>
<string name="screen_notification_settings_mode_mentions">"Mainimiste alusel"</string>
<string name="screen_notification_settings_notification_section_title">"Teavita mind"</string>
<string name="screen_notification_settings_room_mention_label">"Teavita mind @jututoa puhul"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Teavituste saamiseks palun muuda oma %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"süsteemi seadistusi"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Süsteemi teavitused on välja lülitatud"</string>
<string name="screen_notification_settings_title">"Teavitused"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Tõuketeadete ajalugu"</string>
<string name="troubleshoot_notifications_entry_point_section">"Veaotsing"</string>
<string name="troubleshoot_notifications_entry_point_title">"Teavituste veaotsing"</string>
</resources>
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Aukeratu jakinarazpenak nola jaso"</string>
<string name="screen_advanced_settings_developer_mode">"Garatzaile modua"</string>
<string name="screen_advanced_settings_developer_mode_description">"Gaitu garatzaileentzako ezaugarrietarako eta funtzionalitateetarako sarbidea izateko."</string>
<string name="screen_advanced_settings_media_compression_description">"Igo argazkiak eta bideoak azkarrago eta murriztu datuen erabilera"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimizatu multimediaren kalitatea"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderazioa eta Segurtasuna"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimizatu irudien igoera-kalitatea"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Handia (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Txikia (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Ertaina (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Bideoen igoera-kalitatea"</string>
<string name="screen_advanced_settings_push_provider_android">"Push jakinarazpen hornitzailea"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Desgaitu testu aberatseko editorea Markdown eskuz idazteko."</string>
<string name="screen_advanced_settings_send_read_receipts">"Irakurketa-agiriak"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Desaktibatutaz gero, ez zaizkio inori bidaliko mezuak irakurri izanaren agiriak. Beste erabiltzaile batzuen irakurketa-agiriak jasoko dituzu oraindik ere."</string>
<string name="screen_advanced_settings_share_presence">"Partekatu presentzia"</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Ezkutatu beti"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Erakutsi beti"</string>
<string name="screen_advanced_settings_view_source_description">"Gaitu aukera mezuaren iturria denbora-lerroan ikusteko."</string>
<string name="screen_blocked_users_empty">"Ez duzu erabiltzailerik blokeatu"</string>
<string name="screen_blocked_users_unblock_alert_action">"Desblokeatu"</string>
<string name="screen_blocked_users_unblock_alert_description">"Beraien mezu guztiak berriro ikusteko aukera izango duzu."</string>
<string name="screen_blocked_users_unblock_alert_title">"Desblokeatu erabiltzailea"</string>
<string name="screen_blocked_users_unblocking">"Desblokeatzen…"</string>
<string name="screen_edit_profile_display_name">"Pantaila-izena"</string>
<string name="screen_edit_profile_display_name_placeholder">"Zure pantaila-izena"</string>
<string name="screen_edit_profile_error">"Errore ezezagun bat aurkitu da eta ezin izan da informazioa aldatu."</string>
<string name="screen_edit_profile_error_title">"Ezin da profila eguneratu"</string>
<string name="screen_edit_profile_title">"Editatu profila"</string>
<string name="screen_edit_profile_updating_details">"Profila eguneratzen…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Ezarpen gehiago"</string>
<string name="screen_notification_settings_calls_label">"Audio eta bideo deiak"</string>
<string name="screen_notification_settings_configuration_mismatch">"Konfigurazioa ez dator bat"</string>
<string name="screen_notification_settings_direct_chats">"Txat zuzenak"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Errorea gertatu da jakinarazpen-ezarpena eguneratzean."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Mezu guztiak"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Aipamenak eta hitz gakoak soilik"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Zuzeneko txatetan, jakinarazi"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Taldeko txatetan, jakinarazi"</string>
<string name="screen_notification_settings_enable_notifications">"Gaitu jakinarazpenak gailu honetan"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfigurazioa ez da zuzendu; saiatu berriro."</string>
<string name="screen_notification_settings_group_chats">"Taldeko txatak"</string>
<string name="screen_notification_settings_invite_for_me_label">"Gonbidapenak"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Zure zerbitzaria ez da bateragarria enkriptatutako gelen aukerarekin; litekeena da gela batzuetako jakinarazpenak ez jasotzea."</string>
<string name="screen_notification_settings_mentions_section_title">"Aipamenak"</string>
<string name="screen_notification_settings_mode_all">"Guztia"</string>
<string name="screen_notification_settings_mode_mentions">"Aipamenak"</string>
<string name="screen_notification_settings_notification_section_title">"Jakinarazi"</string>
<string name="screen_notification_settings_room_mention_label">"Jakinarazi @gelan"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Jakinarazpenak jasotzeko, aldatu %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"sistemaren ezarpenak"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Sistemaren jakinarazpenak desaktibatuta daude"</string>
<string name="screen_notification_settings_title">"Jakinarazpenak"</string>
</resources>
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_title">"بهبود تجریهٔ تماستان"</string>
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"گزینش چگونگی دریافت آگاهی"</string>
<string name="screen_advanced_settings_developer_mode">"حالت توسعه‌دهنده"</string>
<string name="screen_advanced_settings_developer_mode_description">"دسترسی به ویژگی ها و عملکردها را برای توسعه دهندگان فعال کنید."</string>
<string name="screen_advanced_settings_element_call_base_url">"نشانی پایهٔ تماس المنتی سفارشی"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"تنظمی نشانی پایه‌‌ای سفارشی برای تماس المنتی."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL نامعتبر، لطفا مطمئن شوید که پروتکل (http/https) و آدرس صحیح را درج کرده اید."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"نهفتن چهرک‌ها در درخواست‌های دعوت اتاق"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"نهفتن رسانه در خط زمانی"</string>
<string name="screen_advanced_settings_labs">"آزمایشگاه‌ها"</string>
<string name="screen_advanced_settings_media_compression_title">"بهینه سازی کیفیت رسانه"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"نظارت و امنیت"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"زیاد (۱۰۸۰ت)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"کم (۴۸۰ت)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"استاندارد (۷۲۰ت)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"کیفیت بارگذاری ویدیو"</string>
<string name="screen_advanced_settings_push_provider_android">"فراهم کنندهٔ آگاهی‌های ارسالی"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"از کار انداختن ویرایشگر متن غنی یا نوشتن دستی مارک‌دون."</string>
<string name="screen_advanced_settings_send_read_receipts">"رسید‌های خواندن"</string>
<string name="screen_advanced_settings_share_presence">"هم‌رسانی حضور"</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"نهفتن همیشگی"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"نمایش همیشگی"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"در اتاق‌های خصوصی"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"رسانه‌های نهفته همواره خواهند توانست با زدن رویشان نمایان شوند"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"نمایش رسانه در خط زمانی"</string>
<string name="screen_blocked_users_empty">"هیچ کاربر مسدودی ندارید"</string>
<string name="screen_blocked_users_unblock_alert_action">"رفع انسداد"</string>
<string name="screen_blocked_users_unblock_alert_description">"قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید."</string>
<string name="screen_blocked_users_unblock_alert_title">"رفع انسداد کاربر"</string>
<string name="screen_blocked_users_unblocking">"رفع کردن انسداد…"</string>
<string name="screen_edit_profile_display_name">"نام نمایشی"</string>
<string name="screen_edit_profile_display_name_placeholder">"نام نمایشیتان"</string>
<string name="screen_edit_profile_error">"خطایی ناشناخته رخ داد و اطّلاعات نتوانستند تغییر کنند."</string>
<string name="screen_edit_profile_error_title">"ناتوان در به‌روز کردن نمایه"</string>
<string name="screen_edit_profile_title">"ویرایش نمایه"</string>
<string name="screen_edit_profile_updating_details">"به‌روز کردن نمایه…"</string>
<string name="screen_labs_title">"آزمایشگاه‌ها"</string>
<string name="screen_notification_settings_additional_settings_section_title">"تنظیمات اضافی"</string>
<string name="screen_notification_settings_calls_label">"تماس‌های صوتی و تصویری"</string>
<string name="screen_notification_settings_configuration_mismatch">"نامتطابقت در پیکربندی"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"تنظمیات آگاهی را ساده کرده‌ایم تا یافتن انتخاب‌ها را ساده‌تر کنیم. برهی تنظمیات سفارسی که در گذشته گزیده‌اید این‌جا نشان داده نمی‌شوند؛ ولی همچنن فعّالند.
با ادامه داد ممکن است برخی تنظیماتتان تغییر کنند."</string>
<string name="screen_notification_settings_direct_chats">"گپ‌های مستقیم"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"تنظیمات سفارشی برای هر گپ"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"هنگام به‌روز کردن تنظیمات آگاهی خطایی رخ داد."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"همهٔ پیام‌ها"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"فقط اشاره‌ها و کلیدواژگان"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"آگاهی در گپ‌های مستقیم برای"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"آگاهی در گپ‌های گروهی برای"</string>
<string name="screen_notification_settings_enable_notifications">"به کار انداختن آگاهی‌ها روی این افزاره"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"پیکربندی درست نشد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_notification_settings_group_chats">"گپ‌های گروهی"</string>
<string name="screen_notification_settings_invite_for_me_label">"دعوت‌ها"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"کارساز خانگیتان از این گزینه در اتاق‌های رمز شده پشتیبانی نمی‌کند. ممکن است در برخی اتاق‌ها آگاه نشوید."</string>
<string name="screen_notification_settings_mentions_section_title">"اشاره‌ها"</string>
<string name="screen_notification_settings_mode_all">"همه"</string>
<string name="screen_notification_settings_mode_mentions">"اشاره‌ها"</string>
<string name="screen_notification_settings_notification_section_title">"آگاه کردنم برای"</string>
<string name="screen_notification_settings_room_mention_label">"آگاه کردنم برای @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"برای گرفتن آگاهی‌ها لطفاً%1$sتان را تغییر دهید."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"تنظیمات سامانه"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"آگاهی‌های سامانه‌ای خاموش شدند"</string>
<string name="screen_notification_settings_title">"آگاهی‌ها"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"تاریخچهٔ آگاهی‌های ارسالی"</string>
<string name="troubleshoot_notifications_entry_point_section">"رفع‌اشکال"</string>
<string name="troubleshoot_notifications_entry_point_title">"رفع‌اشکال آگاهی‌ها"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Valitse, miten haluat vastaanottaa ilmoituksia"</string>
<string name="screen_advanced_settings_developer_mode">"Kehittäjätila"</string>
<string name="screen_advanced_settings_developer_mode_description">"Ottamalla käyttöön pääset käsiksi kehittäjille tarkoitettuihin ominaisuuksiin."</string>
<string name="screen_advanced_settings_element_call_base_url">"Mukautettu Element Call URL-osoite"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Aseta mukautettu URL-osoite Element Callille."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Virheellinen URL-osoite. Varmista, että sisällytät protokollan (http/https) ja oikean osoitteen."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Piilota huoneiden avatarit kutsuista"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Piilota median esikatselu aikajanalla"</string>
<string name="screen_advanced_settings_labs">"Labrat"</string>
<string name="screen_advanced_settings_media_compression_description">"Lähetä valokuvia ja videoita nopeammin ja vähennä datan käyttöä."</string>
<string name="screen_advanced_settings_media_compression_title">"Optimoi median laatu"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderointi ja Turvallisuus"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimoi kuvat automaattisesti nopeampia lähetysnopeuksia ja pienempiä tiedostokokoja varten."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimoi kuvien lähetyslaatu"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Napauta tästä vaihtaaksesi."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Korkea (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Matala (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Normaali (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Videon lähetyslaatu"</string>
<string name="screen_advanced_settings_push_provider_android">"Push-ilmoitusten tarjoaja"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Ota rikastettu tekstieditori pois käytöstä, jotta voit kirjoittaa Markdownia manuaalisesti."</string>
<string name="screen_advanced_settings_send_read_receipts">"Lukukuittaukset"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Jos tämä on poissa päältä, sinun lukukuittauksia ei lähetetä kenellekään. Vastaanotat silti lukukuittauksia muilta käyttäjiltä."</string>
<string name="screen_advanced_settings_share_presence">"Jaa läsnäolo"</string>
<string name="screen_advanced_settings_share_presence_description">"Jos tämä on poissa päältä, et lähetä tai vastaanota lukukuittauksia tai kirjoitusilmotuksia."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Piilota aina"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Näytä aina"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Yksityisissä huoneissa"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Piilotetun median voi aina näyttää napauttamalla sitä"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Näytä media aikajanalla"</string>
<string name="screen_advanced_settings_view_source_description">"Ota käyttöön mahdollisuus tarkastella viestin lähdettä aikajanalla."</string>
<string name="screen_blocked_users_empty">"Et ole estänyt ketään"</string>
<string name="screen_blocked_users_unblock_alert_action">"Poista esto"</string>
<string name="screen_blocked_users_unblock_alert_description">"Näet jälleen kaikki heidän lähettämänsä viestit."</string>
<string name="screen_blocked_users_unblock_alert_title">"Poista käyttäjän esto"</string>
<string name="screen_blocked_users_unblocking">"Poistetaan estoa…"</string>
<string name="screen_edit_profile_display_name">"Näyttönimi"</string>
<string name="screen_edit_profile_display_name_placeholder">"Näyttönimesi"</string>
<string name="screen_edit_profile_error">"Tuntematon virhe tapahtui, eikä tietoja voitu muuttaa."</string>
<string name="screen_edit_profile_error_title">"Profiilin muokkaaminen ei onnistunut"</string>
<string name="screen_edit_profile_title">"Muokkaa profiilia"</string>
<string name="screen_edit_profile_updating_details">"Muokataan profiilia…"</string>
<string name="screen_labs_enable_threads">"Ota käyttöön viestiketjuvastaukset"</string>
<string name="screen_labs_enable_threads_description">"Sovellus käynnistyy uudelleen muutoksen käyttöönottamiseksi."</string>
<string name="screen_labs_header_description">"Kokeile uusimpia kehitteillä olevia ideoitamme. Nämä ominaisuudet eivät ole vielä valmiita; ne voivat olla epävakaita ja muuttua."</string>
<string name="screen_labs_header_title">"Kokeilunhaluinen olo?"</string>
<string name="screen_labs_title">"Labrat"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Lisäasetukset"</string>
<string name="screen_notification_settings_calls_label">"Ääni- ja videopuheluista"</string>
<string name="screen_notification_settings_configuration_mismatch">"Konfiguraatio ei täsmää"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Olemme yksinkertaistaneet ilmoitusasetuksia, jotta vaihtoehdot olisi helpompi löytää. Joitakin aiemmin valitsemiasi asetuksia ei näytetä tässä, mutta ne ovat edelleen voimassa.
Jos jatkat, jotkin asetukset saattavat muuttua."</string>
<string name="screen_notification_settings_direct_chats">"Yksityiskeskusteluissa"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Keskustelukohtaiset asetukset"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Ilmoitusasetusten muokkaamisessa tapahtui virhe."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Kaikista viesteistä"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Vain maininnoista ja avainsanoista"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Yksityiskeskusteluissa, ilmoita minulle"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Ryhmäkeskusteluissa, ilmoita minulle"</string>
<string name="screen_notification_settings_enable_notifications">"Ota ilmoitukset käyttöön tällä laitteella"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Määritystä ei ole korjattu, yritä uudelleen."</string>
<string name="screen_notification_settings_group_chats">"Ryhmäkeskusteluissa"</string>
<string name="screen_notification_settings_invite_for_me_label">"Kutsut"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Kotipalvelimesi ei tue tätä vaihtoehtoa salatuissa huoneissa, joten et ehkä saa ilmoitusta joissakin huoneissa."</string>
<string name="screen_notification_settings_mentions_section_title">"Maininnat"</string>
<string name="screen_notification_settings_mode_all">"Kaikki"</string>
<string name="screen_notification_settings_mode_mentions">"Maininnat"</string>
<string name="screen_notification_settings_notification_section_title">"Ilmoita minulle"</string>
<string name="screen_notification_settings_room_mention_label">"Ilmoita minulle @room-maininnoista"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Jos haluat saada ilmoituksia, vaihda %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"järjestelmäsi asetuksia"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Järjestelmän ilmoitukset on poissa päältä"</string>
<string name="screen_notification_settings_title">"Ilmoitukset"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Push-historia"</string>
<string name="troubleshoot_notifications_entry_point_section">"Vianmääritys"</string>
<string name="troubleshoot_notifications_entry_point_title">"Ilmoitusten vianmääritys"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé."</string>
<string name="full_screen_intent_banner_title">"Améliorez votre expérience dappel"</string>
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Choisissez le mode de réception des notifications"</string>
<string name="screen_advanced_settings_developer_mode">"Mode développeur"</string>
<string name="screen_advanced_settings_developer_mode_description">"Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs."</string>
<string name="screen_advanced_settings_element_call_base_url">"URL de base pour Element Call personnalisée"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Configurer une URL de base pour Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL invalide, assurez-vous dinclure le protocol (http/https) et ladresse correcte."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Masquer les avatars des salons dans les invitations"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Masquer les aperçus des médias dans les discussions"</string>
<string name="screen_advanced_settings_labs">"Expérimental"</string>
<string name="screen_advanced_settings_media_compression_description">"Téléchargez des photos et des vidéos plus rapidement et réduisez la consommation de données"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimisez la qualité des médias"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Modération et sécurité"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimiser automatiquement les images pour des envois plus rapides et des tailles de fichiers plus petites."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimiser la qualité des images envoyées"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Appuyez ici pour changer."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Haute définition (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Basse résolution (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Résolution standard (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Qualité des vidéos envoyées"</string>
<string name="screen_advanced_settings_push_provider_android">"Fournisseur de Push"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown."</string>
<string name="screen_advanced_settings_send_read_receipts">"Accusés de lecture"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"En cas de désactivation, vos accusés de lecture ne seront pas envoyés aux autres membres. Vous verrez toujours les accusés des autres membres."</string>
<string name="screen_advanced_settings_share_presence">"Partager la présence"</string>
<string name="screen_advanced_settings_share_presence_description">"Si cette option est désactivée, vous ne pourrez ni envoyer ni recevoir de confirmations de lecture ni de notifications de saisie."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Toujours cacher"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Toujours montrer"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Dans les salons privés"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Un média caché peut toujours être affiché en cliquant dessus"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Afficher les médias dans les discussions."</string>
<string name="screen_advanced_settings_view_source_description">"Activer cette option pour pouvoir voir la source des messages dans la discussion."</string>
<string name="screen_blocked_users_empty">"Vous navez bloqué personne"</string>
<string name="screen_blocked_users_unblock_alert_action">"Débloquer"</string>
<string name="screen_blocked_users_unblock_alert_description">"Vous pourrez à nouveau voir tous ses messages."</string>
<string name="screen_blocked_users_unblock_alert_title">"Débloquer lutilisateur"</string>
<string name="screen_blocked_users_unblocking">"Déblocage…"</string>
<string name="screen_edit_profile_display_name">"Pseudonyme"</string>
<string name="screen_edit_profile_display_name_placeholder">"Votre pseudonyme"</string>
<string name="screen_edit_profile_error">"Une erreur inconnue sest produite et les informations nont pas pu être modifiées."</string>
<string name="screen_edit_profile_error_title">"Impossible de mettre à jour le profil"</string>
<string name="screen_edit_profile_title">"Modifier le profil"</string>
<string name="screen_edit_profile_updating_details">"Mise à jour du profil…"</string>
<string name="screen_labs_enable_threads">"Activez les fils de discussion."</string>
<string name="screen_labs_enable_threads_description">"Un changement entraînera le redémarrage de lapplication."</string>
<string name="screen_labs_header_description">"Découvrez nos dernières idées en cours de développement. Ces fonctionnalités ne sont pas encore finalisées; elles peuvent être instables et évoluer."</string>
<string name="screen_labs_header_title">"Envie dexpérimenter?"</string>
<string name="screen_labs_title">"Expérimental"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Réglages supplémentaires"</string>
<string name="screen_notification_settings_calls_label">"Appels audio et vidéo"</string>
<string name="screen_notification_settings_configuration_mismatch">"Incompatibilité de configuration"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Nous avons simplifié les paramètres des notifications pour que les options soient plus faciles à trouver. Certains paramètres personnalisés que vous avez choisis par le passé ne sont pas affichés ici, mais ils sont toujours actifs.
Si vous continuez, il est possible que certains de vos paramètres soient modifiés."</string>
<string name="screen_notification_settings_direct_chats">"Discussions directes"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Paramétrage personnalisé par salon"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Une erreur sest produite lors de la mise à jour du paramètre de notification."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Tous les messages"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Mentions et mots clés uniquement"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Sur les discussions directes, prévenez-moi pour"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Lors de discussions de groupe, prévenez-moi pour"</string>
<string name="screen_notification_settings_enable_notifications">"Activer les notifications sur cet appareil"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"La configuration na pas été corrigée, veuillez réessayer."</string>
<string name="screen_notification_settings_group_chats">"Discussions de groupe"</string>
<string name="screen_notification_settings_invite_for_me_label">"Invitations"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Votre serveur daccueil ne supporte pas cette option pour les salons chiffrés, vous pourriez ne pas être notifié(e) dans certains salons."</string>
<string name="screen_notification_settings_mentions_section_title">"Mentions"</string>
<string name="screen_notification_settings_mode_all">"Tous"</string>
<string name="screen_notification_settings_mode_mentions">"Mentions"</string>
<string name="screen_notification_settings_notification_section_title">"Prévenez-moi pour"</string>
<string name="screen_notification_settings_room_mention_label">"Prévenez-moi si un message contient \"@room\""</string>
<string name="screen_notification_settings_system_notifications_action_required">"Pour recevoir des notifications, veuillez modifier votre %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"paramètres du système"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Les notifications du système sont désactivées"</string>
<string name="screen_notification_settings_title">"Notifications"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Historique des Push"</string>
<string name="troubleshoot_notifications_entry_point_section">"Dépannage"</string>
<string name="troubleshoot_notifications_entry_point_title">"Dépanner les notifications"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Válassza ki az értesítések fogadási módját"</string>
<string name="screen_advanced_settings_developer_mode">"Fejlesztői mód"</string>
<string name="screen_advanced_settings_developer_mode_description">"Engedélyezze, hogy elérje a fejlesztőknek szánt funkciókat."</string>
<string name="screen_advanced_settings_element_call_base_url">"Egyéni Element Call alapwebcím"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Egyéni alapwebcím beállítása az Element Callhoz."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Érvénytelen webcím, győződjön meg arról, hogy szerepel-e benne a protokoll (http/https), és hogy helyes-e a cím."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Profilképek elrejtése a szobameghívókban"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Médiaelőnézetek elrejtése az idővonalon"</string>
<string name="screen_advanced_settings_labs">"Kísérletek"</string>
<string name="screen_advanced_settings_media_compression_description">"Töltse fel gyorsabban a fényképeket és videókat, valamint csökkentse az adatforgalmat"</string>
<string name="screen_advanced_settings_media_compression_title">"Média minőségének optimalizálása"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderálás és biztonság"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Képek automatikus optimalizációja a gyorsabb feltöltések és kisebb fájlméretek érdekében."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Képfeltöltési minőség optimalizációja"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Koppintson a megváltoztatáshoz."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Magas (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Alacsony (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Szokásos (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Feltöltött videó minősége"</string>
<string name="screen_advanced_settings_push_provider_android">"Leküldéses értesítések szolgáltatója"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"A formázott szöveges szerkesztő letiltása, hogy kézzel írhasson Markdownt."</string>
<string name="screen_advanced_settings_send_read_receipts">"Olvasási visszaigazolások"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Ha ki van kapcsolva, az olvasási visszaigazolások nem lesznek elküldve senkinek. A többi felhasználó olvasási visszaigazolását továbbra is meg fogja kapni."</string>
<string name="screen_advanced_settings_share_presence">"Jelenlét megosztása"</string>
<string name="screen_advanced_settings_share_presence_description">"Ha ki van kapcsolva, nem tud olvasási visszaigazolást vagy írási értesítést küldeni és fogadni"</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Elrejtés mindig"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Megjelenítés mindig"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Privát szobákban"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"A rejtett médiatartalmak koppintással jeleníthetők meg"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Média megjelenítése az idővonalon"</string>
<string name="screen_advanced_settings_view_source_description">"Engedélyezze a beállítást az üzenet forrásának megjelenítéséhez az idővonalon."</string>
<string name="screen_blocked_users_empty">"Nincsenek letiltott felhasználók"</string>
<string name="screen_blocked_users_unblock_alert_action">"Letiltás feloldása"</string>
<string name="screen_blocked_users_unblock_alert_description">"Újra látni fogja az összes üzenetét."</string>
<string name="screen_blocked_users_unblock_alert_title">"Felhasználó letiltásának feloldása"</string>
<string name="screen_blocked_users_unblocking">"Tiltás feloldása…"</string>
<string name="screen_edit_profile_display_name">"Megjelenítendő név"</string>
<string name="screen_edit_profile_display_name_placeholder">"Saját megjelenítendő név"</string>
<string name="screen_edit_profile_error">"Ismeretlen hiba történt, és az információ módosítása nem sikerült."</string>
<string name="screen_edit_profile_error_title">"Nem sikerült frissíteni a profilt"</string>
<string name="screen_edit_profile_title">"Profil szerkesztése"</string>
<string name="screen_edit_profile_updating_details">"Profil frissítése…"</string>
<string name="screen_labs_enable_threads">"Üzenetszál válaszok engedélyezése"</string>
<string name="screen_labs_enable_threads_description">"Az alkalmazás újraindul, hogy a változás érvénybe lépjen."</string>
<string name="screen_labs_header_description">"Próbálja ki legújabb fejlesztéseinket. Ezek a funkciók még nem véglegesek, instabilak lehetnek és változhatnak."</string>
<string name="screen_labs_header_title">"Kísérletezni szeretne?"</string>
<string name="screen_labs_title">"Kísérletek"</string>
<string name="screen_notification_settings_additional_settings_section_title">"További beállítások"</string>
<string name="screen_notification_settings_calls_label">"Hang- és videóhívások"</string>
<string name="screen_notification_settings_configuration_mismatch">"Konfigurációs eltérés"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Egyszerűsítettük az értesítési beállításokat, hogy könnyebben megtalálhatók legyenek a lehetőségek. A korábban kiválasztott egyéni beállítások némelyike nem jelenik meg itt, de továbbra is aktív.
Ha folytatja, egyes beállítások megváltozhatnak."</string>
<string name="screen_notification_settings_direct_chats">"Közvetlen csevegések"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Egyéni beállítás csevegésenként"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Hiba történt az értesítési beállítás frissítésekor."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Összes üzenet"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Csak említések és kulcsszavak"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Közvetlen csevegéseknél értesítés ezekről:"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Csoportos csevegésekben értesítés ezekről:"</string>
<string name="screen_notification_settings_enable_notifications">"Értesítések engedélyezése ezen az eszközön"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"A konfiguráció nem lett kijavítva, próbálja újra."</string>
<string name="screen_notification_settings_group_chats">"Csoportos csevegések"</string>
<string name="screen_notification_settings_invite_for_me_label">"Meghívók"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, előfordulhat, hogy egyes szobákban nem kap értesítést."</string>
<string name="screen_notification_settings_mentions_section_title">"Említések"</string>
<string name="screen_notification_settings_mode_all">"Összes"</string>
<string name="screen_notification_settings_mode_mentions">"Említések"</string>
<string name="screen_notification_settings_notification_section_title">"Értesítés ezekről:"</string>
<string name="screen_notification_settings_room_mention_label">"Értesítés a @room említésekor"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Az értesítések fogadásához kérjük, módosítsa a %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"rendszerbeállításokat"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"A rendszerértesítések ki vannak kapcsolva"</string>
<string name="screen_notification_settings_title">"Értesítések"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Leküldéses értesítések előzmények"</string>
<string name="troubleshoot_notifications_entry_point_section">"Hibaelhárítás"</string>
<string name="troubleshoot_notifications_entry_point_title">"Értesítések hibaelhárítása"</string>
</resources>
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Pilih cara menerima notifikasi"</string>
<string name="screen_advanced_settings_developer_mode">"Mode pengembang"</string>
<string name="screen_advanced_settings_developer_mode_description">"Aktifkan untuk mengakses fitur dan fungsi untuk para pengembang."</string>
<string name="screen_advanced_settings_element_call_base_url">"URL dasar Element Call khusus"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Tetapkan URL dasar khusus untuk Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL tidak valid, pastikan Anda menyertakan protokol (http/https) dan alamat yang benar."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Sembunyikan avatar dalam permintaan undangan ruangan"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Sembunyikan pratinjau media pada lini masa"</string>
<string name="screen_advanced_settings_media_compression_description">"Unggah foto dan video lebih cepat dan kurangi penggunaan data"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimalkan kualitas media"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderasi dan Keamanan"</string>
<string name="screen_advanced_settings_push_provider_android">"Penyedia notifikasi dorongan"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Nonaktifkan penyunting teks kaya untuk mengetik Markdown secara manual."</string>
<string name="screen_advanced_settings_send_read_receipts">"Laporan dibaca"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Jika dimatikan, laporan dibaca Anda tidak akan dikirim kepada siapa pun. Anda masih akan menerima laporan dibaca dari pengguna lain."</string>
<string name="screen_advanced_settings_share_presence">"Bagikan presensi"</string>
<string name="screen_advanced_settings_share_presence_description">"Jika dimatikan, Anda tidak akan dapat mengirim atau menerima laporan dibaca atau notifikasi pengetikan"</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Selalu sembunyikan"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Selalu tampilkan"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Dalam ruangan privat"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Media tersembunyi selalu dapat ditampilkan dengan mengetuknya"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Tampilkan media pada lini masa"</string>
<string name="screen_advanced_settings_view_source_description">"Aktifkan opsi untuk melihat sumber pesan dalam lini masa."</string>
<string name="screen_blocked_users_empty">"Anda tidak memiliki pengguna yang diblokir"</string>
<string name="screen_blocked_users_unblock_alert_action">"Buka blokir"</string>
<string name="screen_blocked_users_unblock_alert_description">"Anda akan dapat melihat semua pesan dari mereka lagi."</string>
<string name="screen_blocked_users_unblock_alert_title">"Buka blokir pengguna"</string>
<string name="screen_blocked_users_unblocking">"Membatalkan pemblokiran…"</string>
<string name="screen_edit_profile_display_name">"Nama tampilan"</string>
<string name="screen_edit_profile_display_name_placeholder">"Nama tampilan Anda"</string>
<string name="screen_edit_profile_error">"Terjadi kesalahan yang tidak diketahui dan informasi tidak dapat diubah."</string>
<string name="screen_edit_profile_error_title">"Tidak dapat memperbarui profil"</string>
<string name="screen_edit_profile_title">"Sunting profil"</string>
<string name="screen_edit_profile_updating_details">"Memperbarui profil…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Pengaturan tambahan"</string>
<string name="screen_notification_settings_calls_label">"Panggilan audio dan video"</string>
<string name="screen_notification_settings_configuration_mismatch">"Ketidakcocokan pengaturan"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Kami telah menyederhanakan Pengaturan Pemberitahuan untuk membuat opsi lebih mudah ditemukan.
Beberapa pengaturan khusus yang Anda pilih di masa lalu tidak ditampilkan di sini, tetapi masih aktif.
Jika Anda melanjutkan, beberapa pengaturan Anda dapat berubah."</string>
<string name="screen_notification_settings_direct_chats">"Obrolan langsung"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Pengaturan khusus per obrolan"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Terjadi kesalahan saat memperbarui pengaturan pemberitahuan."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Semua pesan"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Sebutan dan Kata Kunci saja"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Di obrolan langsung, beri tahu saya tentang"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Di obrolan grup, beri tahu tentang"</string>
<string name="screen_notification_settings_enable_notifications">"Aktifkan pemberitahuan di perangkat ini"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Pengaturan belum diperbaiki, silakan coba lagi."</string>
<string name="screen_notification_settings_group_chats">"Obrolan grup"</string>
<string name="screen_notification_settings_invite_for_me_label">"Undangan"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda mungkin tidak diberi tahu dalam beberapa ruangan."</string>
<string name="screen_notification_settings_mentions_section_title">"Sebutan"</string>
<string name="screen_notification_settings_mode_all">"Semua"</string>
<string name="screen_notification_settings_mode_mentions">"Sebutan"</string>
<string name="screen_notification_settings_notification_section_title">"Beri tahu saya tentang"</string>
<string name="screen_notification_settings_room_mention_label">"Beri tahu saya pada @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Untuk menerima pemberitahuan, silakan ubah %1$s Anda."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"pengaturan sistem"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Pemberitahuan sistem dimatikan"</string>
<string name="screen_notification_settings_title">"Notifikasi"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Riwayat dorongan"</string>
<string name="troubleshoot_notifications_entry_point_section">"Pemecahan masalah"</string>
<string name="troubleshoot_notifications_entry_point_title">"Pecahkan masalah notifikasi"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Scegli come ricevere le notifiche"</string>
<string name="screen_advanced_settings_developer_mode">"Modalità sviluppatore"</string>
<string name="screen_advanced_settings_developer_mode_description">"Attiva per avere accesso alle funzionalità per sviluppatori."</string>
<string name="screen_advanced_settings_element_call_base_url">"URL base di Element Call personalizzato"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Imposta un URL di base personalizzato per Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL non valido, assicurati di includere il protocollo (http/https) e l\'indirizzo corretto."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Nascondi gli avatar nelle richieste di invito alle stanze"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Nascondi le anteprime dei media nelle conversazioni"</string>
<string name="screen_advanced_settings_labs">"Labs"</string>
<string name="screen_advanced_settings_media_compression_description">"Carica foto e video più velocemente e riduci l\'utilizzo dei dati"</string>
<string name="screen_advanced_settings_media_compression_title">"Ottimizza la qualità dei contenuti multimediali"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderazione e Sicurezza"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Ottimizza automaticamente le immagini per caricamenti più rapidi e file di dimensioni ridotte."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Ottimizza la qualità del caricamento delle immagini"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Tocca qui per cambiarla."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Alta (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Bassa (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Standard (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Qualità del caricamento video"</string>
<string name="screen_advanced_settings_push_provider_android">"Fornitore di notifiche push"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Disattiva l\'editor di testo avanzato per scrivere manualmente in Markdown"</string>
<string name="screen_advanced_settings_send_read_receipts">"Conferme di visualizzazione"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Se disattivato, le tue conferme di visualizzazione non verranno inviate a nessuno. Riceverai comunque conferme di visualizzazione da altri utenti."</string>
<string name="screen_advanced_settings_share_presence">"Condividi presenza online"</string>
<string name="screen_advanced_settings_share_presence_description">"Se disattivato, non potrai né inviare né ricevere conferme di lettura o notifiche di scrittura."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Nascondi sempre"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Mostra sempre"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Nelle stanze private"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Un file multimediale nascosto può sempre essere visualizzato toccandolo"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Mostra i media nella conversazione"</string>
<string name="screen_advanced_settings_view_source_description">"Attiva l\'opzione per visualizzare il codice sorgente del messaggio nella conversazione."</string>
<string name="screen_blocked_users_empty">"Non hai utenti bloccati"</string>
<string name="screen_blocked_users_unblock_alert_action">"Sblocca"</string>
<string name="screen_blocked_users_unblock_alert_description">"Potrai vedere di nuovo tutti i suoi messaggi."</string>
<string name="screen_blocked_users_unblock_alert_title">"Sblocca utente"</string>
<string name="screen_blocked_users_unblocking">"Sblocco in corso…"</string>
<string name="screen_edit_profile_display_name">"Nome visualizzato"</string>
<string name="screen_edit_profile_display_name_placeholder">"Il tuo nome visualizzato"</string>
<string name="screen_edit_profile_error">"Si è verificato un errore sconosciuto e non è stato possibile modificare le informazioni."</string>
<string name="screen_edit_profile_error_title">"Impossibile aggiornare il profilo"</string>
<string name="screen_edit_profile_title">"Modifica profilo"</string>
<string name="screen_edit_profile_updating_details">"Aggiornamento del profilo…"</string>
<string name="screen_labs_enable_threads">"Abilita le risposte alle discussioni"</string>
<string name="screen_labs_enable_threads_description">"L\'app si riavvierà per applicare questa modifica."</string>
<string name="screen_labs_header_description">"Prova le nostre ultime idee in fase di sviluppo. Queste funzionalità non sono definitive; potrebbero essere instabili e soggette a modifiche."</string>
<string name="screen_labs_header_title">"Hai voglia di sperimentare?"</string>
<string name="screen_labs_title">"Labs"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Impostazioni aggiuntive"</string>
<string name="screen_notification_settings_calls_label">"Chiamate audio e video"</string>
<string name="screen_notification_settings_configuration_mismatch">"Mancata corrispondenza di configurazione"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Abbiamo semplificato le impostazioni di notifica per rendere le opzioni più facili da trovare. Alcune impostazioni personalizzate che hai scelto in passato non sono mostrate qui, ma sono ancora attive.
Se procedi, alcune delle tue impostazioni potrebbero cambiare."</string>
<string name="screen_notification_settings_direct_chats">"Conversazioni dirette"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Impostazione personalizzata per conversazione"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Si è verificato un errore durante l\'aggiornamento delle impostazioni di notifica."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Tutti i messaggi"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Solo menzioni e parole chiave"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Nelle conversazioni dirette, avvisami per"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Nelle conversazioni di gruppo, avvisami per"</string>
<string name="screen_notification_settings_enable_notifications">"Attiva le notifiche su questo dispositivo"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"La configurazione non è stata corretta, riprova."</string>
<string name="screen_notification_settings_group_chats">"Chat di gruppo"</string>
<string name="screen_notification_settings_invite_for_me_label">"Inviti"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Il tuo homeserver non supporta questa opzione nelle stanze crifrate, quindi potresti non ricevere notifiche in alcune stanze."</string>
<string name="screen_notification_settings_mentions_section_title">"Menzioni"</string>
<string name="screen_notification_settings_mode_all">"Tutto"</string>
<string name="screen_notification_settings_mode_mentions">"Menzioni"</string>
<string name="screen_notification_settings_notification_section_title">"Avvisami per"</string>
<string name="screen_notification_settings_room_mention_label">"Avvisami con @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Per ricevere notifiche, modifica le tue %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"impostazioni di sistema"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Notifiche di sistema disattivate"</string>
<string name="screen_notification_settings_title">"Notifiche"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Cronologia push"</string>
<string name="troubleshoot_notifications_entry_point_section">"Risoluzione dei problemi"</string>
<string name="troubleshoot_notifications_entry_point_title">"Risoluzione di problemi delle notifiche"</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="screen_advanced_settings_choose_distributor_dialog_title_android">"აირჩიეთ, თუ როგორ გსურთ შეტყობინებების მიღება"</string>
<string name="screen_advanced_settings_developer_mode">"დეველოპერის რეჟიმი"</string>
<string name="screen_advanced_settings_developer_mode_description">"ჩართეთ დეველოპერების ფუნქციებზე წვდომა."</string>
<string name="screen_advanced_settings_element_call_base_url">"მორგებული Element-ის ზარის საბაზისო URL"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"დააყენეთ საბაზისო URL Element-ის ზარებისათვის."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"არასწორი URL, გთხოვთ, დარწმუნდეთ, რომ შეიტანეთ პროტოკოლი (http/https) და სწორი მისამართი."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"გამორთეთ მდიდარი ტექსტის რედაქტორი, რათა ხელით აკრიფოთ Markdown."</string>
<string name="screen_advanced_settings_send_read_receipts">"წაკითხვის შეტყობინებები"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"გამორთვის შემთხვევაში სხვები ვერ მიიღებენ თქვენი წაკითხვის შეტყობინებებს. თქვენ მაინც მიიღებთ სხვების წაკითხვის შეტყობინებებს."</string>
<string name="screen_advanced_settings_share_presence">"მყოფობის გაზიარება"</string>
<string name="screen_advanced_settings_share_presence_description">"გამორთვის შემთხვევაში თქვენ ვერ მიიღებთ და ვერ გაგზავნით წაკითხვის ან წერის შეტყობინებებს."</string>
<string name="screen_advanced_settings_view_source_description">"ჩართეთ ოპცია რათა შეტყობინების წყაროს დროის ისტორია ნახოთ."</string>
<string name="screen_blocked_users_empty">"თქვენ არ დაგიბლოკავთ მომხმარებლები"</string>
<string name="screen_blocked_users_unblock_alert_action">"განბლოკვა"</string>
<string name="screen_blocked_users_unblock_alert_description">"თქვენ კვლავ შეძლებთ მათგან ყველა შეტყობინების ნახვას."</string>
<string name="screen_blocked_users_unblock_alert_title">"Მომხმარებლის განბლოკვა"</string>
<string name="screen_blocked_users_unblocking">"განბლოკვა…"</string>
<string name="screen_edit_profile_display_name">"ნაჩვენები სახელი"</string>
<string name="screen_edit_profile_display_name_placeholder">"თქვენი ნაჩვენები სახელი"</string>
<string name="screen_edit_profile_error">"დაფიქსირდა უცნობი შეცდომა და ინფორმაციის შეცვლა ვერ მოხერხდა."</string>
<string name="screen_edit_profile_error_title">"პროფილის განახლება ვერ მოხერხდა"</string>
<string name="screen_edit_profile_title">"Პროფილის რედაქტირება"</string>
<string name="screen_edit_profile_updating_details">"პროფილის განახლება…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"დამატებითი პარამეტრები"</string>
<string name="screen_notification_settings_calls_label">"აუდიო და ვიდეო ზარები"</string>
<string name="screen_notification_settings_configuration_mismatch">"კონფიგურაციის შეუსაბამობა"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"ჩვენ გავამარტივეთ შეტყობინებების პარამეტრები, რათა გაგიადვილოთ ვარიანტების პოვნა.
თქვენ მიერ წარსულში არჩეული ზოგიერთი მორგებული პარამეტრი აქ არ არის ნაჩვენები, მაგრამ ისინი კვლავ აქტიურია. თუ გააგრძელებთ, თქვენი ზოგიერთი პარამეტრი შეიძლება შეიცვალოს."</string>
<string name="screen_notification_settings_direct_chats">"პირდაპირი ჩატები"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"მორგებული პარამეტრი ჩატზე"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"შეტყობინებების პარამეტრის განახლებისას მოხდა შეცდომა."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"ყველა შეტყობინება"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"მხოლოდ ხსენებები და საკვანძო სიტყვები"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"პირდაპირ ჩატებზე, შემატყობინეთ:"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"ჯგუფურ ჩატებზე, შემატყობინეთ:"</string>
<string name="screen_notification_settings_enable_notifications">"შეტყობინებების ჩართვა ამ მოწყობილობაზე"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"კონფიგურაცია არ გამოსწორებულა, გთხოვთ, კვლავ სცადოთ."</string>
<string name="screen_notification_settings_group_chats">"ჯგუფური ჩატები"</string>
<string name="screen_notification_settings_invite_for_me_label">"მოსაწვევები"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"თქვენი სახლის სერვერი არ უჭერს მხარს ამ პარამეტრს დაშიფრულ ოთახებში, ზოგიერთ ოთახში შეიძლება არ მიიღოთ შეტყობინება."</string>
<string name="screen_notification_settings_mentions_section_title">"ხსენებები"</string>
<string name="screen_notification_settings_mode_all">"ყველა"</string>
<string name="screen_notification_settings_mode_mentions">"ხსენებები"</string>
<string name="screen_notification_settings_notification_section_title">"ჩემი შეტყობინება შემდეგისთვის:"</string>
<string name="screen_notification_settings_room_mention_label">"ჩემი შეტყობინება @room-ზე"</string>
<string name="screen_notification_settings_system_notifications_action_required">"შეტყობინებების მისაღებად გთხოვთ შეცვალოთ %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"სისტემის პარამეტრები"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"სისტემის შეტყობინებები გამორთულია"</string>
<string name="screen_notification_settings_title">"შეტყობინებები"</string>
<string name="troubleshoot_notifications_entry_point_section">"პრობლემების გადაჭრა"</string>
<string name="troubleshoot_notifications_entry_point_title">"პრობლემების გადაჭრის შეტყობინებები"</string>
</resources>
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"중요한 전화를 놓치지 않으려면 휴대폰이 잠겨 있을 때 전체 화면 알림을 허용하도록 설정을 변경하세요."</string>
<string name="full_screen_intent_banner_title">"통화 경험을 향상시키세요"</string>
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"어떻게 알림을 받을지 선택하기"</string>
<string name="screen_advanced_settings_developer_mode">"개발자 모드"</string>
<string name="screen_advanced_settings_developer_mode_description">"개발자가 기능에 액세스할 수 있도록 합니다."</string>
<string name="screen_advanced_settings_element_call_base_url">"사용자 정의 요소 호출 베이스 URL"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Element Call에 대한 사용자 지정 기본 URL을 설정하세요."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL이 잘못되었습니다. 프로토콜(http/https)과 올바른 주소를 포함했는지 확인하세요."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"방 초대 요청에서 아바타 숨기기"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"타임라인에서 미디어 미리 보기 숨기기"</string>
<string name="screen_advanced_settings_media_compression_description">"사진과 동영상을 더 빠르게 업로드하고 데이터 사용량을 줄이세요"</string>
<string name="screen_advanced_settings_media_compression_title">"미디어 품질 최적화"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"중재와 안전"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"더 빠른 업로드와 더 작은 파일 크기에 맞춰 이미지를 자동으로 최적화합니다."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"이미지 업로드 품질 최적화"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. 여기를 탭하여 변경하세요."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"고화질 (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"저화질 (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"표준 화질 (720p)
"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"비디오 업로드 품질"</string>
<string name="screen_advanced_settings_push_provider_android">"푸시 알림 제공자"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"마크다운을 직접 입력하려면 서식 있는 텍스트 편집기를 비활성화하세요."</string>
<string name="screen_advanced_settings_send_read_receipts">"읽기 확인"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"이 기능을 해제하면 읽기 확인이 누구에게도 전송되지 않습니다. 다른 사용자의 읽기 확인은 계속 수신됩니다."</string>
<string name="screen_advanced_settings_share_presence">"현재 상태 공유"</string>
<string name="screen_advanced_settings_share_presence_description">"이 기능을 해제하면 읽기 확인 및 타이핑 알림을 보내거나 받을 수 없습니다."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"항상 숨기기"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"항상 표시"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"비공개 방에서"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"숨겨진 미디어는 터치로 표시할 수 있습니다."</string>
<string name="screen_advanced_settings_show_media_timeline_title">"타임라인에 미디어 표시"</string>
<string name="screen_advanced_settings_view_source_description">"타임라인에서 메시지 소스를 볼 수 있는 옵션을 활성화합니다."</string>
<string name="screen_blocked_users_empty">"차단된 사용자가 없습니다."</string>
<string name="screen_blocked_users_unblock_alert_action">"차단 해제"</string>
<string name="screen_blocked_users_unblock_alert_description">"그들로부터 보낸 모든 메시지를 다시 볼 수 있게 됩니다."</string>
<string name="screen_blocked_users_unblock_alert_title">"사용자 차단 해제"</string>
<string name="screen_blocked_users_unblocking">"차단 해제 중…"</string>
<string name="screen_edit_profile_display_name">"표시되는 이름"</string>
<string name="screen_edit_profile_display_name_placeholder">"내 표시되는 이름"</string>
<string name="screen_edit_profile_error">"알 수 없는 오류가 발생하여 정보를 변경할 수 없습니다."</string>
<string name="screen_edit_profile_error_title">"프로필을 업데이트할 수 없음"</string>
<string name="screen_edit_profile_title">"프로필 수정"</string>
<string name="screen_edit_profile_updating_details">"프로필 업데이트 중…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"추가 설정"</string>
<string name="screen_notification_settings_calls_label">"음성 및 동영상 통화"</string>
<string name="screen_notification_settings_configuration_mismatch">"구성 불일치"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"알림 설정을 간소화하여 옵션을 더 쉽게 찾을 수 있도록 했습니다. 과거에 선택한 일부 맞춤 설정은 여기에서 표시되지 않지만, 여전히 활성화되어 있습니다.
계속 진행하면 일부 설정이 변경될 수 있습니다."</string>
<string name="screen_notification_settings_direct_chats">"직접 채팅"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"채팅별 맞춤 설정"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"알림 설정 업데이트 중 오류가 발생했습니다."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"모든 메시지"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"언급 및 키워드만"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"다이렉트 채팅에서 알림 받기"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"그룹 채팅에서 나에게 알림을 보내세요"</string>
<string name="screen_notification_settings_enable_notifications">"이 장치에서 알림 사용"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"설정이 수정되지 않았습니다. 다시 시도해 주세요."</string>
<string name="screen_notification_settings_group_chats">"그룹 채팅"</string>
<string name="screen_notification_settings_invite_for_me_label">"초대"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"귀하의 홈서버는 암호화된 방에서 이 옵션을 지원하지 않으므로, 일부 방에서는 알림이 표시되지 않을 수 있습니다."</string>
<string name="screen_notification_settings_mentions_section_title">"언급"</string>
<string name="screen_notification_settings_mode_all">"모두"</string>
<string name="screen_notification_settings_mode_mentions">"언급"</string>
<string name="screen_notification_settings_notification_section_title">"나에게 알려주세요"</string>
<string name="screen_notification_settings_room_mention_label">@room 에서 알림 받기</string>
<string name="screen_notification_settings_system_notifications_action_required">"알림을 받으려면 %1$s 을 변경해 주세요."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"시스템 설정"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"시스템 알림이 꺼져 있습니다."</string>
<string name="screen_notification_settings_title">"알림"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"푸시 기록"</string>
<string name="troubleshoot_notifications_entry_point_section">"문제 해결"</string>
<string name="troubleshoot_notifications_entry_point_title">"문제 해결 알림"</string>
</resources>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Pasirinkite, kaip gauti pranešimus"</string>
<string name="screen_blocked_users_unblock_alert_action">"Atblokuoti"</string>
<string name="screen_blocked_users_unblock_alert_description">"Vėl galėsite matyti visas iš jų gautas žinutes."</string>
<string name="screen_blocked_users_unblock_alert_title">"Atblokuoti vartotoją"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Velg hvordan du vil motta varsler"</string>
<string name="screen_advanced_settings_developer_mode">"Utviklermodus"</string>
<string name="screen_advanced_settings_developer_mode_description">"Aktiver for å få tilgang til funksjoner og funksjonalitet for utviklere."</string>
<string name="screen_advanced_settings_element_call_base_url">"Egendefinert Element Call base URL"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Angi en egendefinert base URL for Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Ugyldig URL. Pass på at du inkluderer protokollen (http/https) og riktig adresse."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Skjul avatarer i invitasjonsforespørsler til rom"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Skjul forhåndsvisninger av medier på tidslinjen"</string>
<string name="screen_advanced_settings_labs">"Prøvefunksjoner"</string>
<string name="screen_advanced_settings_media_compression_description">"Last opp bilder og videoer raskere og reduser databruken"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimaliser mediekvaliteten"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderasjon og sikkerhet"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimaliser bilder automatisk for raskere opplastinger og mindre filstørrelser."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimaliser kvaliteten på bildeopplasting"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Trykk her for å endre."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Høy (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Lav (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Standard (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Kvalitet på videoopplasting"</string>
<string name="screen_advanced_settings_push_provider_android">"Leverandør av pushvarsling"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Deaktiver rik tekstredigering for å skrive Markdown manuelt."</string>
<string name="screen_advanced_settings_send_read_receipts">"Lesebekreftelser"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Hvis slått av, sendes ikke lesebekreftelsene dine til noen. Du vil fortsatt motta lesebekreftelser fra andre brukere."</string>
<string name="screen_advanced_settings_share_presence">"Del tilstedeværelse"</string>
<string name="screen_advanced_settings_share_presence_description">"Hvis slått av, kan du ikke sende eller motta lesebekreftelser eller skrivevarsler."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Skjul alltid"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Vis alltid"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"I private rom"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Et skjult medium kan alltid vises ved å trykke på det"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Vis medier i tidslinjen"</string>
<string name="screen_advanced_settings_view_source_description">"Aktiver alternativet for å vise meldingskilden på tidslinjen."</string>
<string name="screen_blocked_users_empty">"Du har ingen blokkerte brukere"</string>
<string name="screen_blocked_users_unblock_alert_action">"Fjern blokkering"</string>
<string name="screen_blocked_users_unblock_alert_description">"Du vil kunne se alle meldingene fra dem igjen."</string>
<string name="screen_blocked_users_unblock_alert_title">"Fjern blokkering av bruker"</string>
<string name="screen_blocked_users_unblocking">"Fjerner blokkering …"</string>
<string name="screen_edit_profile_display_name">"Visningsnavn"</string>
<string name="screen_edit_profile_display_name_placeholder">"Ditt visningsnavn"</string>
<string name="screen_edit_profile_error">"Det oppstod en ukjent feil, og informasjonen kunne ikke endres."</string>
<string name="screen_edit_profile_error_title">"Kan ikke oppdatere profilen"</string>
<string name="screen_edit_profile_title">"Rediger profil"</string>
<string name="screen_edit_profile_updating_details">"Oppdaterer profilen…"</string>
<string name="screen_labs_enable_threads">"Aktiver trådsvar"</string>
<string name="screen_labs_enable_threads_description">"Appen vil starte på nytt for å implementere denne endringen."</string>
<string name="screen_labs_header_description">"Prøv ut våre nyeste ideer under utvikling. Disse funksjonene er ikke ferdig utviklet; de kan være ustabile og kan endres."</string>
<string name="screen_labs_header_title">"Lyst til å prøve noe nytt?"</string>
<string name="screen_labs_title">"Prøvefunksjoner"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Ytterligere innstillinger"</string>
<string name="screen_notification_settings_calls_label">"Lyd- og videosamtaler"</string>
<string name="screen_notification_settings_configuration_mismatch">"Uoverensstemmelse i konfigurasjonen"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Vi har forenklet varslingsinnstillingene for å gjøre det lettere å finne alternativene. Noen av de egendefinerte innstillingene du har valgt tidligere, vises ikke her, men de er fortsatt aktive.
Hvis du fortsetter, kan noen av innstillingene dine endres."</string>
<string name="screen_notification_settings_direct_chats">"Direkte chatter"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Egendefinert innstilling per chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Det oppstod en feil under oppdatering av varslingsinnstillingen."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Alle meldinger"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Bare omtaler og nøkkelord"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"På direkte chatter, varsle meg for"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"I gruppechatter, varsle meg om"</string>
<string name="screen_notification_settings_enable_notifications">"Aktiver varsler på denne enheten"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfigurasjonen er ikke korrigert, prøv igjen."</string>
<string name="screen_notification_settings_group_chats">"Gruppechatter"</string>
<string name="screen_notification_settings_invite_for_me_label">"Invitasjoner"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Hjemmeserveren din støtter ikke dette alternativet i krypterte rom, og det kan hende at du ikke blir varslet i enkelte rom."</string>
<string name="screen_notification_settings_mentions_section_title">"Omtaler"</string>
<string name="screen_notification_settings_mode_all">"Alle"</string>
<string name="screen_notification_settings_mode_mentions">"Omtaler"</string>
<string name="screen_notification_settings_notification_section_title">"Varsle meg om"</string>
<string name="screen_notification_settings_room_mention_label">"Gi meg varsel på @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"For å motta varsler, vennligst endre %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systeminnstillinger"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systemvarsler er slått av"</string>
<string name="screen_notification_settings_title">"Varslinger"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Push-historikk"</string>
<string name="troubleshoot_notifications_entry_point_section">"Feilsøk"</string>
<string name="troubleshoot_notifications_entry_point_title">"Feilsøk varsler"</string>
</resources>
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Kies hoe je meldingen wilt ontvangen"</string>
<string name="screen_advanced_settings_developer_mode">"Ontwikkelaarsmodus"</string>
<string name="screen_advanced_settings_developer_mode_description">"Schakel in om toegang te krijgen tot tools en functies voor ontwikkelaars."</string>
<string name="screen_advanced_settings_element_call_base_url">"Aangepaste basis-URL voor Element Call"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Stel een aangepaste basis-URL in voor Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Ongeldige URL, zorg ervoor dat je het protocol (http/https) en het juiste adres invult."</string>
<string name="screen_advanced_settings_push_provider_android">"Push-meldingen provider"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Schakel de uitgebreide tekstverwerker uit om Markdown handmatig te typen."</string>
<string name="screen_advanced_settings_send_read_receipts">"Leesbevestigingen"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Indien uitgeschakeld worden er geen leesbevestigingen verstuurd. Je ontvangt nog steeds leesbevestigingen van andere gebruikers."</string>
<string name="screen_advanced_settings_share_presence">"Aanwezigheid delen"</string>
<string name="screen_advanced_settings_share_presence_description">"Indien uitgeschakeld kun je geen leesbevestigingen en typmeldingen verzenden of ontvangen."</string>
<string name="screen_advanced_settings_view_source_description">"Schakel optie in om de berichtbron in de tijdlijn te bekijken."</string>
<string name="screen_blocked_users_empty">"Je hebt geen geblokkeerde gebruikers."</string>
<string name="screen_blocked_users_unblock_alert_action">"Deblokkeren"</string>
<string name="screen_blocked_users_unblock_alert_description">"Je zult alle berichten van hen weer kunnen zien."</string>
<string name="screen_blocked_users_unblock_alert_title">"Gebruiker deblokkeren"</string>
<string name="screen_blocked_users_unblocking">"Deblokkeren…"</string>
<string name="screen_edit_profile_display_name">"Weergavenaam"</string>
<string name="screen_edit_profile_display_name_placeholder">"Je weergavenaam"</string>
<string name="screen_edit_profile_error">"Er is een onbekende fout opgetreden en de informatie kon niet worden gewijzigd."</string>
<string name="screen_edit_profile_error_title">"Kan profiel niet bijwerken"</string>
<string name="screen_edit_profile_title">"Profiel bewerken"</string>
<string name="screen_edit_profile_updating_details">"Profiel bijwerken…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Aanvullende instellingen"</string>
<string name="screen_notification_settings_calls_label">"Audio- en videogesprekken"</string>
<string name="screen_notification_settings_configuration_mismatch">"Configuratie komt niet overeen"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"We hebben de instellingen voor meldingen vereenvoudigd, zodat je de opties gemakkelijker kunt vinden. Sommige instellingen die je in het verleden hebt aangepast, worden hier niet getoond, maar zijn nog steeds actief.
Als je doorgaat, kunnen sommige van je instellingen veranderen."</string>
<string name="screen_notification_settings_direct_chats">"Directe chats"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Aangepaste instelling per chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Er is een fout opgetreden bij het bijwerken van de meldingsinstelling."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Alle berichten"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Alleen vermeldingen en trefwoorden"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Bij directe chats, stuur me een melding voor"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Bij groep chats, stuur me een melding voor"</string>
<string name="screen_notification_settings_enable_notifications">"Meldingen op dit apparaat inschakelen"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"De configuratie is niet gecorrigeerd. Probeer het opnieuw."</string>
<string name="screen_notification_settings_group_chats">"Groep chats"</string>
<string name="screen_notification_settings_invite_for_me_label">"Uitnodigingen"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Je homeserver ondersteunt deze optie niet in versleutelde kamers; in sommige kamers krijg je mogelijk geen meldingen."</string>
<string name="screen_notification_settings_mentions_section_title">"Vermeldingen"</string>
<string name="screen_notification_settings_mode_all">"Alles"</string>
<string name="screen_notification_settings_mode_mentions">"Vermeldingen"</string>
<string name="screen_notification_settings_notification_section_title">"Stuur me een melding voor"</string>
<string name="screen_notification_settings_room_mention_label">"Stuur me een melding bij @kamer"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Wijzig je %1$s om meldingen te ontvangen."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systeeminstellingen"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systeemmeldingen uitgeschakeld"</string>
<string name="screen_notification_settings_title">"Meldingen"</string>
<string name="troubleshoot_notifications_entry_point_section">"Problemen oplossen"</string>
<string name="troubleshoot_notifications_entry_point_title">"Problemen met meldingen oplossen"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"Upewnij się, że nie pominiesz żadnego połączenia. Zmień swoje ustawienia i zezwól na powiadomienia na blokadzie ekranu."</string>
<string name="full_screen_intent_banner_title">"Popraw jakość swoich rozmów"</string>
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Wybierz sposób otrzymywania powiadomień"</string>
<string name="screen_advanced_settings_developer_mode">"Tryb programisty"</string>
<string name="screen_advanced_settings_developer_mode_description">"Włącz, aby uzyskać dostęp do funkcji dla deweloperów."</string>
<string name="screen_advanced_settings_element_call_base_url">"Własny bazowy URL dla połączeń Element"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Ustaw własny bazowy URL dla połączeń Element"</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Nieprawidłowy adres URL, upewnij się, że zawiera protokół (http/https) i poprawny adres."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Ukryj awatary w prośbach o dołączenie do pokoju"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Ukryj podglądy multimediów na osi czasu"</string>
<string name="screen_advanced_settings_labs">"Laboratoria"</string>
<string name="screen_advanced_settings_media_compression_description">"Przesyłaj zdjęcia i filmy szybciej, zmniejszając zużycie danych"</string>
<string name="screen_advanced_settings_media_compression_title">"Optymalizuj jakość multimediów"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderacja i bezpieczeństwo"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Automatycznie optymalizuj obrazy, aby szybciej je przesyłać i zmniejszać rozmiar plików."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Zoptymalizuj jakość przesyłania obrazów"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Dotknij tutaj, aby zmienić."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Wysoka (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Niska (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Standardowa (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Jakość przesyłania wideo"</string>
<string name="screen_advanced_settings_push_provider_android">"Dostawca powiadomień push"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Wyłącz edytor tekstu bogatego, aby pisać tekst Markdown ręcznie."</string>
<string name="screen_advanced_settings_send_read_receipts">"Potwierdzenia odczytania"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Gdy wyłączona, Twoje potwierdzenia odczytania nie zostaną wysłane. Potwierdzenia od innych wciąż będą odbierane."</string>
<string name="screen_advanced_settings_share_presence">"Udostępnij obecność"</string>
<string name="screen_advanced_settings_share_presence_description">"Gdy wyłączona, nie będziesz mógł wysyłać lub odbierać potwierdzeń odczytu ani powiadomień pisania."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Zawsze ukrywaj"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Zawsze pokazuj"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"W pokojach prywatnych"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Ukryte media można zawsze wyświetlić, dotykając ich"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Pokaż media na osi czasu"</string>
<string name="screen_advanced_settings_view_source_description">"Włącz opcję, aby wyświetlić źródło wiadomości na osi czasu."</string>
<string name="screen_blocked_users_empty">"Nie blokujesz żadnych użytkowników"</string>
<string name="screen_blocked_users_unblock_alert_action">"Odblokuj"</string>
<string name="screen_blocked_users_unblock_alert_description">"Będziesz mógł ponownie zobaczyć wszystkie wiadomości od tego użytkownika."</string>
<string name="screen_blocked_users_unblock_alert_title">"Odblokuj użytkownika"</string>
<string name="screen_blocked_users_unblocking">"Odblokowuję…"</string>
<string name="screen_edit_profile_display_name">"Wyświetlana nazwa"</string>
<string name="screen_edit_profile_display_name_placeholder">"Twoja wyświetlana nazwa"</string>
<string name="screen_edit_profile_error">"Wystąpił nieznany błąd przez co nie można było zmienić informacji."</string>
<string name="screen_edit_profile_error_title">"Nie można zaktualizować profilu"</string>
<string name="screen_edit_profile_title">"Edytuj profil"</string>
<string name="screen_edit_profile_updating_details">"Aktualizuję profil…"</string>
<string name="screen_labs_enable_threads">"Włącz odpowiedzi w wątkach"</string>
<string name="screen_labs_enable_threads_description">"Aplikacja uruchomi się ponownie, aby zastosować tę zmianę."</string>
<string name="screen_labs_header_description">"Wypróbuj nasze najnowsze pomysły w fazie rozwoju. Funkcje te nie są jeszcze sfinalizowane; mogą być niestabilne i ulec zmianie."</string>
<string name="screen_labs_header_title">"Chcesz poeksperymentować?"</string>
<string name="screen_labs_title">"Laboratoria"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Dodatkowe ustawienia"</string>
<string name="screen_notification_settings_calls_label">"Połączenia audio i wideo"</string>
<string name="screen_notification_settings_configuration_mismatch">"Niezgodność konfiguracji"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Uprościliśmy Ustawienia powiadomień, aby ułatwić nawigowanie między opcjami. Niektóre ustawienia, które wybrałeś mogły zniknąć, lecz są wciąż aktywne.
Niektóre ustawienia mogą ulec zmianie, jeśli kontynuujesz."</string>
<string name="screen_notification_settings_direct_chats">"Czaty prywatne"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Ustawienia własne wybranego czatu"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Wystąpił błąd podczas aktualizacji ustawienia powiadomień."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Wszystkie wiadomości"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Tylko wzmianki i słowa kluczowe"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Na czatach prywatnych, powiadamiaj mnie przez"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Na czatach grupowych powiadamiaj mnie przez"</string>
<string name="screen_notification_settings_enable_notifications">"Włącz powiadomienia na tym urządzeniu"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfiguracja nie została poprawiona, spróbuj ponownie."</string>
<string name="screen_notification_settings_group_chats">"Czaty grupowe"</string>
<string name="screen_notification_settings_invite_for_me_label">"Zaproszenia"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Twój serwer domowy nie wspiera tej opcji w pokojach szyfrowanych, możesz nie otrzymać powiadomień z niektórych pokoi."</string>
<string name="screen_notification_settings_mentions_section_title">"Wzmianki"</string>
<string name="screen_notification_settings_mode_all">"Wszystkie"</string>
<string name="screen_notification_settings_mode_mentions">"Wzmianki"</string>
<string name="screen_notification_settings_notification_section_title">"Powiadamiaj mnie przez"</string>
<string name="screen_notification_settings_room_mention_label">"Powiadom mnie na @pokój"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Aby otrzymywać powiadomienia, zmień swoje%1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"ustawienia systemowe"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Powiadomienia systemowe wyłączone"</string>
<string name="screen_notification_settings_title">"Powiadomienia"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Historia powiadomień Push"</string>
<string name="troubleshoot_notifications_entry_point_section">"Rozwiązywanie problemów"</string>
<string name="troubleshoot_notifications_entry_point_title">"Rozwiązywanie problemów powiadomień"</string>
</resources>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"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_advanced_settings_choose_distributor_dialog_title_android">"Escolha como receber notificações"</string>
<string name="screen_advanced_settings_developer_mode">"Modo de desenvolvedor"</string>
<string name="screen_advanced_settings_developer_mode_description">"Ative para ter acesso a recursos e funcionalidades para desenvolvedores."</string>
<string name="screen_advanced_settings_element_call_base_url">"URL base do Element Call personalizada"</string>
<string name="screen_advanced_settings_element_call_base_url_description">"Defina uma URL base personalizada para o Element Call."</string>
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL inválida, por favor verifique se o protocolo (http/https) está incluso e o endereço correto."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Ocultar avatares em solicitações de convite para salas"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Ocultar pré-visualizações de mídia na linha do tempo"</string>
<string name="screen_advanced_settings_labs">"Experimentos"</string>
<string name="screen_advanced_settings_media_compression_description">"Envie fotos e vídeos com mais rapidez e reduza o uso de dados"</string>
<string name="screen_advanced_settings_media_compression_title">"Otimizar a qualidade da mídia"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Moderação e segurança"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Otimizar automaticamente as imagens para envios mais rápidos e arquivos com tamanhos menores."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Otimizar qualidade de envio de imagens"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Toque aqui para alterar."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Alta (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Baixa (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Normal (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Qualidade de envio de vídeos"</string>
<string name="screen_advanced_settings_push_provider_android">"Provedor de notificações push"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Desative o editor de rich text para digitar Markdown manualmente."</string>
<string name="screen_advanced_settings_send_read_receipts">"Confirmações de leitura"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Se desligado, suas confirmações de leitura não serão enviadas para ninguém. Você ainda receberá confirmações de leitura de outros usuários."</string>
<string name="screen_advanced_settings_share_presence">"Compartilhar presença"</string>
<string name="screen_advanced_settings_share_presence_description">"Se desligado, você não poderá enviar ou receber confirmações de leitura ou notificações de digitação."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Ocultar sempre"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Mostrar sempre"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"Em salas privadas"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Uma mídia oculta sempre pode ser exibida se você tocar nela"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Mostrar mídia na linha do tempo"</string>
<string name="screen_advanced_settings_view_source_description">"Ative a opção para visualizar o fonte da mensagem na linha do tempo."</string>
<string name="screen_blocked_users_empty">"Você não tem usuários bloqueados"</string>
<string name="screen_blocked_users_unblock_alert_action">"Desbloquear"</string>
<string name="screen_blocked_users_unblock_alert_description">"Você poderá ver todas as mensagens desta pessoa novamente."</string>
<string name="screen_blocked_users_unblock_alert_title">"Desbloquear usuário"</string>
<string name="screen_blocked_users_unblocking">"Desbloqueando…"</string>
<string name="screen_edit_profile_display_name">"Nome de exibição"</string>
<string name="screen_edit_profile_display_name_placeholder">"Seu nome de exibição"</string>
<string name="screen_edit_profile_error">"Ocorreu um erro desconhecido e as informações não puderam ser alteradas."</string>
<string name="screen_edit_profile_error_title">"Não foi possível atualizar o perfil"</string>
<string name="screen_edit_profile_title">"Editar perfil"</string>
<string name="screen_edit_profile_updating_details">"Atualizando o perfil…"</string>
<string name="screen_labs_enable_threads">"Ativar respostas de tópicos"</string>
<string name="screen_labs_enable_threads_description">"O app será reiniciado para aplicar esta mudança."</string>
<string name="screen_labs_header_description">"Teste as nossas mais novas ideias em desenvolvimento. Esses recursos não estão finalizados; podem estar instáveis, e podem mudar."</string>
<string name="screen_labs_header_title">"Se sentindo experimental?"</string>
<string name="screen_labs_title">"Experimentos"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Configurações adicionais"</string>
<string name="screen_notification_settings_calls_label">"Chamadas de áudio e vídeo"</string>
<string name="screen_notification_settings_configuration_mismatch">"Não correspondência de configuração"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Simplificamos as configurações de notificações para facilitar a localização das opções. Algumas configurações personalizadas que você escolheu no passado não são mostradas aqui, mas ainda estão ativas.
Se você continuar, algumas de suas configurações poderão mudar."</string>
<string name="screen_notification_settings_direct_chats">"Conversas diretas"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Configuração personalizada por conversa"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Ocorreu um erro ao atualizar a configuração de notificação."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Todas as mensagens"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Somente menções e palavras-chave"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"Em conversas diretas, me notifique de"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"Em conversas em grupos, me notifique de"</string>
<string name="screen_notification_settings_enable_notifications">"Ativar notificações neste dispositivo"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"A configuração não foi corrigida, tente novamente."</string>
<string name="screen_notification_settings_group_chats">"Conversas em grupo"</string>
<string name="screen_notification_settings_invite_for_me_label">"Convites"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Seu servidor-casa não suporta esta opção em salas criptografadas. Você pode não ser notificado em algumas salas."</string>
<string name="screen_notification_settings_mentions_section_title">"Menções"</string>
<string name="screen_notification_settings_mode_all">"Todos"</string>
<string name="screen_notification_settings_mode_mentions">"Menções"</string>
<string name="screen_notification_settings_notification_section_title">"Me notifique de"</string>
<string name="screen_notification_settings_room_mention_label">"Notifique-me quando usam o @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Para receber notificações, altere as %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"configurações do seu sistema"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Notificações do sistema desativadas"</string>
<string name="screen_notification_settings_title">"Notificações"</string>
<string name="troubleshoot_notifications_entry_point_push_history_title">"Histórico de push"</string>
<string name="troubleshoot_notifications_entry_point_section">"Solução de problemas"</string>
<string name="troubleshoot_notifications_entry_point_title">"Solucionar problemas de notificações"</string>
</resources>

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