Merge branch 'develop' into valere/call/fix_join_button_on_several_items
This commit is contained in:
commit
af47e2b405
376 changed files with 5383 additions and 2535 deletions
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4be10c3bb9900d27a3b406eca0cb902b0ff9cdf90e8e3cf1ae7760aa7c5d47d9
|
||||
size 377446
|
||||
oid sha256:1f1a277e76d351f48ae0041e082525422604fbf41d77fe078112349855dd3d2e
|
||||
size 453512
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -62,7 +63,7 @@ internal fun IconsCompoundPreviewRtl() = ElementTheme {
|
|||
|
||||
@Preview(widthDp = 730, heightDp = 1920)
|
||||
@Composable
|
||||
internal fun IconsCompoundPreviewDark() = ElementTheme(darkTheme = true) {
|
||||
internal fun IconsCompoundPreviewDark() = ElementTheme(theme = Theme.Dark) {
|
||||
IconsCompoundPreview()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.tokens.generated.compoundColorsHcDark
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
|
|
@ -65,7 +66,7 @@ internal fun CompoundSemanticColorsLightHc() = ElementTheme(
|
|||
|
||||
@Preview(heightDp = 2000)
|
||||
@Composable
|
||||
internal fun CompoundSemanticColorsDark() = ElementTheme(darkTheme = true) {
|
||||
internal fun CompoundSemanticColorsDark() = ElementTheme(theme = Theme.Dark) {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
|
|
@ -85,7 +86,7 @@ internal fun CompoundSemanticColorsDark() = ElementTheme(darkTheme = true) {
|
|||
@Preview(heightDp = 2000)
|
||||
@Composable
|
||||
internal fun CompoundSemanticColorsDarkHc() = ElementTheme(
|
||||
darkTheme = true,
|
||||
theme = Theme.Dark,
|
||||
compoundDark = compoundColorsHcDark,
|
||||
) {
|
||||
Surface {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ internal fun AvatarColorsPreviewLight() {
|
|||
@Preview
|
||||
@Composable
|
||||
internal fun AvatarColorsPreviewDark() {
|
||||
ElementTheme(darkTheme = true) {
|
||||
ElementTheme(theme = Theme.Dark) {
|
||||
val chunks = avatarColors().chunked(4)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
for (chunk in chunks) {
|
||||
|
|
|
|||
|
|
@ -77,10 +77,10 @@ internal val LocalCompoundColors = staticCompositionLocalOf { compoundColorsLigh
|
|||
/**
|
||||
* Sets up the theme for the application, or a part of it.
|
||||
*
|
||||
* @param darkTheme whether to use the dark theme or not. If `true`, the dark theme will be used.
|
||||
* @param theme the [Theme] to use. Defaults to [Theme.Dark] or [Theme.Light] based on the system setting.
|
||||
* @param applySystemBarsUpdate whether to update the system bars color scheme or not when the theme changes. It's `true` by default.
|
||||
* This is specially useful when you want to apply an alternate theme to a part of the app but don't want it to affect the system bars.
|
||||
* @param lightStatusBar whether to use a light status bar color scheme or not. By default, it's the opposite of [darkTheme].
|
||||
* @param lightStatusBar whether to use a light status bar color scheme or not. By default, it's `true` for light themes and `false` for dark ones.
|
||||
* @param dynamicColor whether to enable MaterialYou or not. It's `false` by default.
|
||||
* @param compoundLight the [SemanticColors] to use in light theme.
|
||||
* @param compoundDark the [SemanticColors] to use in dark theme.
|
||||
|
|
@ -91,9 +91,9 @@ internal val LocalCompoundColors = staticCompositionLocalOf { compoundColorsLigh
|
|||
*/
|
||||
@Composable
|
||||
fun ElementTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
theme: Theme = if (isSystemInDarkTheme()) Theme.Dark else Theme.Light,
|
||||
applySystemBarsUpdate: Boolean = true,
|
||||
lightStatusBar: Boolean = !darkTheme,
|
||||
lightStatusBar: Boolean = !theme.isDark(),
|
||||
// true to enable MaterialYou
|
||||
dynamicColor: Boolean = false,
|
||||
compoundLight: SemanticColors = compoundColorsLight,
|
||||
|
|
@ -103,8 +103,13 @@ fun ElementTheme(
|
|||
typography: Typography = compoundTypography,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val darkTheme = theme.isDark()
|
||||
val currentCompoundColor = when {
|
||||
darkTheme -> compoundDark
|
||||
darkTheme -> if (theme == Theme.Black) {
|
||||
compoundDark.copy(bgCanvasDefault = Color.Black)
|
||||
} else {
|
||||
compoundDark
|
||||
}
|
||||
else -> compoundLight
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +118,11 @@ fun ElementTheme(
|
|||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> materialColorsDark
|
||||
darkTheme -> if (theme == Theme.Black) {
|
||||
currentCompoundColor.toMaterialColorScheme()
|
||||
} else {
|
||||
materialColorsDark
|
||||
}
|
||||
else -> materialColorsLight
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +139,7 @@ fun ElementTheme(
|
|||
|
||||
if (applySystemBarsUpdate) {
|
||||
val activity = LocalActivity.current as? ComponentActivity
|
||||
LaunchedEffect(statusBarColorScheme, darkTheme, lightStatusBar) {
|
||||
LaunchedEffect(statusBarColorScheme, theme, lightStatusBar) {
|
||||
activity?.enableEdgeToEdge(
|
||||
// For Status bar use the background color of the app
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ fun ForcedDarkElementTheme(
|
|||
}
|
||||
}
|
||||
ElementTheme(
|
||||
darkTheme = true,
|
||||
theme = Theme.Dark,
|
||||
compoundLight = colors.light,
|
||||
compoundDark = colors.dark,
|
||||
lightStatusBar = lightStatusBar,
|
||||
|
|
|
|||
|
|
@ -36,11 +36,15 @@ internal fun MaterialTextPreview() = Row(
|
|||
) {
|
||||
MaterialPreview(
|
||||
modifier = Modifier.weight(1f),
|
||||
darkTheme = false,
|
||||
theme = Theme.Light,
|
||||
)
|
||||
MaterialPreview(
|
||||
modifier = Modifier.weight(1f),
|
||||
darkTheme = true,
|
||||
theme = Theme.Dark,
|
||||
)
|
||||
MaterialPreview(
|
||||
modifier = Modifier.weight(1f),
|
||||
theme = Theme.Black,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +56,7 @@ private data class Model(
|
|||
|
||||
@Composable
|
||||
private fun MaterialPreview(
|
||||
darkTheme: Boolean,
|
||||
theme: Theme,
|
||||
modifier: Modifier = Modifier,
|
||||
) = Column(modifier = modifier) {
|
||||
Text(
|
||||
|
|
@ -60,13 +64,13 @@ private fun MaterialPreview(
|
|||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
text = if (darkTheme) "Dark" else "Light",
|
||||
text = theme.name,
|
||||
color = Color.Black,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
ElementTheme(
|
||||
darkTheme = darkTheme,
|
||||
theme = theme,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ internal fun ColorsSchemeLightHcPreview() = ElementTheme(
|
|||
@Preview(heightDp = 1200)
|
||||
@Composable
|
||||
internal fun ColorsSchemeDarkPreview() = ElementTheme(
|
||||
darkTheme = true,
|
||||
theme = Theme.Dark,
|
||||
) {
|
||||
ColorsSchemePreview(
|
||||
Color.White,
|
||||
|
|
@ -62,7 +62,7 @@ internal fun ColorsSchemeDarkPreview() = ElementTheme(
|
|||
@Preview(heightDp = 1200)
|
||||
@Composable
|
||||
internal fun ColorsSchemeDarkHcPreview() = ElementTheme(
|
||||
darkTheme = true,
|
||||
theme = Theme.Dark,
|
||||
compoundDark = compoundColorsHcDark,
|
||||
) {
|
||||
ColorsSchemePreview(
|
||||
|
|
@ -71,3 +71,15 @@ internal fun ColorsSchemeDarkHcPreview() = ElementTheme(
|
|||
ElementTheme.materialColors,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(heightDp = 1200)
|
||||
@Composable
|
||||
internal fun ColorsSchemeBlackPreview() = ElementTheme(
|
||||
theme = Theme.Black
|
||||
) {
|
||||
ColorsSchemePreview(
|
||||
Color.White,
|
||||
Color.Black,
|
||||
ElementTheme.materialColors,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,21 +16,26 @@ import kotlinx.coroutines.flow.map
|
|||
enum class Theme {
|
||||
System,
|
||||
Dark,
|
||||
Black,
|
||||
Light,
|
||||
}
|
||||
|
||||
private fun Theme.coerceBlackTheme(allowBlackTheme: Boolean): Theme {
|
||||
return if (this == Theme.Black && !allowBlackTheme) Theme.Dark else this
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Theme.isDark(): Boolean {
|
||||
return when (this) {
|
||||
Theme.System -> isSystemInDarkTheme()
|
||||
Theme.Dark -> true
|
||||
Theme.Dark, Theme.Black -> true
|
||||
Theme.Light -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Flow<String?>.mapToTheme(): Flow<Theme> = map {
|
||||
fun Flow<String?>.mapToTheme(allowBlackTheme: Boolean): Flow<Theme> = map {
|
||||
when (it) {
|
||||
null -> Theme.System
|
||||
else -> Theme.valueOf(it)
|
||||
}
|
||||
}.coerceBlackTheme(allowBlackTheme)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import io.element.android.compound.previews.IconsCompoundPreviewRtl
|
|||
import io.element.android.compound.previews.IconsPreview
|
||||
import io.element.android.compound.screenshot.utils.screenshotFile
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Test
|
||||
|
|
@ -56,7 +57,7 @@ class CompoundIconTest {
|
|||
val content: List<@Composable ColumnScope.() -> Unit> = CompoundIcons.all.map {
|
||||
@Composable { Icon(imageVector = it, contentDescription = null) }
|
||||
}
|
||||
ElementTheme(darkTheme = true) {
|
||||
ElementTheme(theme = Theme.Dark) {
|
||||
IconsPreview(
|
||||
title = "Compound Vector Icons",
|
||||
content = content.toImmutableList()
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
|
|||
import io.element.android.compound.previews.ColorsSchemePreview
|
||||
import io.element.android.compound.screenshot.utils.screenshotFile
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
|
@ -51,7 +52,7 @@ class MaterialYouThemeTest {
|
|||
}
|
||||
}
|
||||
captureRoboImage(file = screenshotFile("MaterialYou Theme - Dark.png")) {
|
||||
ElementTheme(dynamicColor = true, darkTheme = true) {
|
||||
ElementTheme(dynamicColor = true, theme = Theme.Dark) {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import app.cash.molecule.RecompositionMode
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -72,4 +73,14 @@ class ThemeTest {
|
|||
assertThat(awaitItem()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapToTheme falls back to dark when black theme is disabled`() = runTest {
|
||||
flowOf(Theme.Black.name)
|
||||
.mapToTheme(allowBlackTheme = false)
|
||||
.test {
|
||||
assertThat(awaitItem()).isEqualTo(Theme.Dark)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.annotations.CoreColorToken
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.tokens.generated.internal.DarkColorTokens
|
||||
import io.element.android.compound.tokens.generated.internal.LightColorTokens
|
||||
import io.element.android.libraries.designsystem.R
|
||||
|
|
@ -50,7 +51,7 @@ fun SunsetPage(
|
|||
) {
|
||||
ElementTheme(
|
||||
// Always use the opposite value of the current theme
|
||||
darkTheme = ElementTheme.isLightTheme,
|
||||
theme = if (ElementTheme.isLightTheme) Theme.Dark else Theme.Light,
|
||||
applySystemBarsUpdate = false,
|
||||
) {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||
|
||||
RoomSelectRoomListItem(36.dp),
|
||||
|
||||
UserPreference(56.dp),
|
||||
UserPreference(52.dp),
|
||||
|
||||
UserHeader(96.dp),
|
||||
UserListItem(36.dp),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import coil3.asImage
|
|||
import coil3.compose.AsyncImagePreviewHandler
|
||||
import coil3.compose.LocalAsyncImagePreviewHandler
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables
|
|||
@Composable
|
||||
@Suppress("ModifierMissing")
|
||||
fun ElementPreview(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
theme: Theme = if (isSystemInDarkTheme()) Theme.Dark else Theme.Light,
|
||||
showBackground: Boolean = true,
|
||||
@DrawableRes
|
||||
drawableFallbackForImages: Int = CommonDrawables.sample_background,
|
||||
|
|
@ -38,7 +39,7 @@ fun ElementPreview(
|
|||
ResourcesCompat.getDrawable(context.resources, drawableFallbackForImages, null)!!.asImage()
|
||||
}
|
||||
) {
|
||||
ElementTheme(darkTheme = darkTheme) {
|
||||
ElementTheme(theme = theme) {
|
||||
if (showBackground) {
|
||||
// If we have a proper contentColor applied we need a Surface instead of a Box
|
||||
Surface(content = content)
|
||||
|
|
|
|||
|
|
@ -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.libraries.designsystem.preview
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.compound.theme.Theme
|
||||
|
||||
@Composable
|
||||
fun ElementPreviewBlack(
|
||||
showBackground: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
ElementPreview(
|
||||
theme = Theme.Black,
|
||||
showBackground = showBackground,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
@ -8,16 +8,22 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.preview
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
@Composable
|
||||
fun ElementPreviewDark(
|
||||
showBackground: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
@DrawableRes
|
||||
drawableFallbackForImages: Int = CommonDrawables.sample_background,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
ElementPreview(
|
||||
darkTheme = true,
|
||||
theme = Theme.Dark,
|
||||
showBackground = showBackground,
|
||||
content = content
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,22 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.preview
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
@Composable
|
||||
fun ElementPreviewLight(
|
||||
showBackground: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
@DrawableRes
|
||||
drawableFallbackForImages: Int = CommonDrawables.sample_background,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
ElementPreview(
|
||||
darkTheme = false,
|
||||
theme = Theme.Light,
|
||||
showBackground = showBackground,
|
||||
content = content
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
@Composable
|
||||
|
|
@ -40,14 +41,14 @@ fun ElementThemedPreview(
|
|||
if (vertical) {
|
||||
Column {
|
||||
ElementPreview(
|
||||
darkTheme = false,
|
||||
theme = Theme.Light,
|
||||
showBackground = showBackground,
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
ElementPreview(
|
||||
darkTheme = true,
|
||||
theme = Theme.Dark,
|
||||
showBackground = showBackground,
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content
|
||||
|
|
@ -56,14 +57,14 @@ fun ElementThemedPreview(
|
|||
} else {
|
||||
Row {
|
||||
ElementPreview(
|
||||
darkTheme = false,
|
||||
theme = Theme.Light,
|
||||
showBackground = showBackground,
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
ElementPreview(
|
||||
darkTheme = true,
|
||||
theme = Theme.Dark,
|
||||
showBackground = showBackground,
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.compound.theme.isDark
|
||||
import io.element.android.compound.theme.mapToTheme
|
||||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
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.preferences.api.store.AppPreferencesStore
|
||||
|
||||
val LocalBuildMeta = staticCompositionLocalOf {
|
||||
|
|
@ -54,21 +55,24 @@ val LocalBuildMeta = staticCompositionLocalOf {
|
|||
@Composable
|
||||
fun ElementThemeApp(
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
featureFlagService: FeatureFlagService,
|
||||
compoundLight: SemanticColors,
|
||||
compoundDark: SemanticColors,
|
||||
buildMeta: BuildMeta,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val theme by remember {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
.collectAsState(initial = Theme.System)
|
||||
val isBlackThemeAllowed by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.AllowBlackTheme)
|
||||
}.collectAsState(initial = false)
|
||||
val theme by remember(isBlackThemeAllowed) {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme(allowBlackTheme = isBlackThemeAllowed)
|
||||
}.collectAsState(initial = Theme.System)
|
||||
LaunchedEffect(theme) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (theme) {
|
||||
Theme.System -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
Theme.Light -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
Theme.Dark -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
Theme.Dark, Theme.Black -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -76,7 +80,7 @@ fun ElementThemeApp(
|
|||
LocalBuildMeta provides buildMeta,
|
||||
) {
|
||||
ElementTheme(
|
||||
darkTheme = theme.isDark(),
|
||||
theme = theme,
|
||||
content = content,
|
||||
compoundLight = compoundLight,
|
||||
compoundDark = compoundDark,
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class DefaultRoomLatestEventFormatter(
|
|||
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
|
||||
}
|
||||
is LiveLocationContent -> {
|
||||
val message = sp.getString(CommonStrings.common_shared_location)
|
||||
val message = sp.getString(CommonStrings.common_shared_live_location)
|
||||
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
|
||||
}
|
||||
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call)
|
||||
|
|
|
|||
|
|
@ -104,14 +104,6 @@ enum class FeatureFlags(
|
|||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
SyncNotificationsWithWorkManager(
|
||||
key = "feature.sync_notifications_with_workmanager",
|
||||
title = "Sync notifications with WorkManager",
|
||||
description = "Use WorkManager to schedule notification sync tasks when a push is received." +
|
||||
" This should improve reliability and battery usage.",
|
||||
defaultValue = { true },
|
||||
isFinished = false,
|
||||
),
|
||||
QrCodeLogin(
|
||||
key = "feature.qr_code_login",
|
||||
title = "QR Code Login",
|
||||
|
|
@ -126,6 +118,13 @@ enum class FeatureFlags(
|
|||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
AllowBlackTheme(
|
||||
key = "feature.allow_black_theme",
|
||||
title = "Allow black theme",
|
||||
description = "Allow selecting the black appearance theme for battery saving on OLED.",
|
||||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
LiveLocationSharing(
|
||||
key = "feature.liveLocationSharing",
|
||||
title = "Live location sharing",
|
||||
|
|
|
|||
|
|
@ -15,10 +15,19 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
data class LiveLocationShare(
|
||||
/** The user who is sharing their location. */
|
||||
val userId: UserId,
|
||||
/** The last known geo URI (e.g., "geo:51.5074,-0.1278"). */
|
||||
val lastGeoUri: String,
|
||||
/** The timestamp of the last location update. */
|
||||
val lastTimestamp: Long,
|
||||
/** Whether the live location share is still active. */
|
||||
val isLive: Boolean,
|
||||
/** The last known location if any. */
|
||||
val lastLocation: LastLocation?,
|
||||
/** The timestamp when location sharing started, in milliseconds.*/
|
||||
val startTimestamp: Long,
|
||||
/** The timestamp when location sharing ends, in milliseconds. */
|
||||
val endTimestamp: Long,
|
||||
)
|
||||
|
||||
data class LastLocation(
|
||||
/** The last known geo URI (e.g., "geo:51.5074,-0.1278"). */
|
||||
val geoUri: String,
|
||||
/** The timestamp of the last location update. */
|
||||
val timestamp: Long,
|
||||
/** The asset of the last location update. */
|
||||
val assetType: AssetType,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -106,13 +106,15 @@ data class FailedToParseStateContent(
|
|||
) : EventContent
|
||||
|
||||
data class LiveLocationContent(
|
||||
val body: String,
|
||||
val isLive: Boolean,
|
||||
val description: String?,
|
||||
val startTimestamp: Long,
|
||||
val timeout: Long,
|
||||
val assetType: AssetType?,
|
||||
val locations: List<LiveLocationInfo>,
|
||||
) : EventContent
|
||||
) : EventContent {
|
||||
val endTimestamp = startTimestamp + timeout
|
||||
}
|
||||
|
||||
data object LegacyCallInviteContent : EventContent
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ dependencies {
|
|||
} else {
|
||||
debugImplementation(libs.matrix.sdk)
|
||||
}
|
||||
implementation(files("libs/rustls-platform-verifier-android.aar"))
|
||||
implementation(projects.libraries.rustlsTls)
|
||||
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.libraries.androidutils)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
Updated rustls-platform-verifier-android.aar using `rustls-platform-verifier-0.1.1.aar`
|
||||
|
|
@ -44,6 +44,8 @@ import io.element.android.libraries.matrix.impl.mapper.map
|
|||
import io.element.android.libraries.matrix.impl.room.history.map
|
||||
import io.element.android.libraries.matrix.impl.room.join.map
|
||||
import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest
|
||||
import io.element.android.libraries.matrix.impl.room.location.liveLocationSharesFlow
|
||||
import io.element.android.libraries.matrix.impl.room.location.timedByExpiry
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
|
||||
import io.element.android.libraries.matrix.impl.room.threads.RustThreadsListService
|
||||
import io.element.android.libraries.matrix.impl.roomdirectory.map
|
||||
|
|
@ -511,7 +513,7 @@ class JoinedRustRoom(
|
|||
}
|
||||
|
||||
override fun subscribeToLiveLocationShares(): Flow<List<LiveLocationShare>> {
|
||||
TODO("Not implemented yet")
|
||||
return innerRoom.liveLocationSharesFlow().timedByExpiry(systemClock::epochMillis)
|
||||
}
|
||||
|
||||
override suspend fun startLiveLocationShare(durationMillis: Long): Result<Unit> = withContext(roomDispatcher) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.matrix.impl.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.LastLocation
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import org.matrix.rustcomponents.sdk.LiveLocationShareListener
|
||||
import org.matrix.rustcomponents.sdk.LiveLocationShareUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomInterface
|
||||
import org.matrix.rustcomponents.sdk.LiveLocationShare as RustLiveLocationShare
|
||||
|
||||
fun RoomInterface.liveLocationSharesFlow(): Flow<List<LiveLocationShare>> {
|
||||
fun MutableList<LiveLocationShare>.applyUpdate(update: LiveLocationShareUpdate) {
|
||||
when (update) {
|
||||
is LiveLocationShareUpdate.Append -> addAll(update.values.map { it.into() })
|
||||
is LiveLocationShareUpdate.Clear -> clear()
|
||||
is LiveLocationShareUpdate.Insert -> add(update.index.toInt(), update.value.into())
|
||||
is LiveLocationShareUpdate.PopBack -> if (isNotEmpty()) removeAt(lastIndex)
|
||||
is LiveLocationShareUpdate.PopFront -> if (isNotEmpty()) removeAt(0)
|
||||
is LiveLocationShareUpdate.PushBack -> add(update.value.into())
|
||||
is LiveLocationShareUpdate.PushFront -> add(0, update.value.into())
|
||||
is LiveLocationShareUpdate.Remove -> removeAt(update.index.toInt())
|
||||
is LiveLocationShareUpdate.Reset -> {
|
||||
clear()
|
||||
addAll(update.values.map { it.into() })
|
||||
}
|
||||
is LiveLocationShareUpdate.Set -> set(update.index.toInt(), update.value.into())
|
||||
is LiveLocationShareUpdate.Truncate -> subList(update.length.toInt(), size).clear()
|
||||
}
|
||||
}
|
||||
return callbackFlow {
|
||||
val liveLocationShares = liveLocationShares()
|
||||
val shares: MutableList<LiveLocationShare> = ArrayList()
|
||||
val taskHandle = liveLocationShares.subscribe(object : LiveLocationShareListener {
|
||||
override fun onUpdate(updates: List<LiveLocationShareUpdate>) {
|
||||
for (update in updates) {
|
||||
shares.applyUpdate(update)
|
||||
}
|
||||
trySend(shares)
|
||||
}
|
||||
})
|
||||
awaitClose {
|
||||
taskHandle.cancelAndDestroy()
|
||||
liveLocationShares.destroy()
|
||||
}
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
}
|
||||
|
||||
private fun RustLiveLocationShare.into(): LiveLocationShare {
|
||||
return LiveLocationShare(
|
||||
userId = UserId(userId),
|
||||
lastLocation = lastLocation?.let {
|
||||
LastLocation(
|
||||
geoUri = it.location.geoUri,
|
||||
timestamp = it.ts.toLong(),
|
||||
assetType = it.location.asset.into(),
|
||||
)
|
||||
},
|
||||
startTimestamp = startTs.toLong(),
|
||||
endTimestamp = (startTs + timeout).toLong()
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.matrix.impl.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Makes sure to filter and emit live location based on the endTimestamp.
|
||||
*/
|
||||
internal fun Flow<List<LiveLocationShare>>.timedByExpiry(
|
||||
currentTimeMillis: () -> Long = System::currentTimeMillis,
|
||||
): Flow<List<LiveLocationShare>> = channelFlow {
|
||||
var timerJob: Job? = null
|
||||
|
||||
fun List<LiveLocationShare>.nextExpiryAfter(timestamp: Long): Long? {
|
||||
return this
|
||||
.asSequence()
|
||||
.map { it.endTimestamp }
|
||||
.filter { it > timestamp }
|
||||
.minOrNull()
|
||||
}
|
||||
|
||||
fun List<LiveLocationShare>.filterLive(): List<LiveLocationShare> {
|
||||
val currentTimeMillis = currentTimeMillis()
|
||||
return filter { it.endTimestamp > currentTimeMillis }
|
||||
}
|
||||
|
||||
fun reschedule(shares: List<LiveLocationShare>) {
|
||||
timerJob?.cancel()
|
||||
timerJob = launch {
|
||||
val currentTimeMillis = currentTimeMillis()
|
||||
val nextExpiry = shares.nextExpiryAfter(currentTimeMillis) ?: return@launch
|
||||
delay((nextExpiry - currentTimeMillis).coerceAtLeast(0))
|
||||
val liveShares = shares.filterLive()
|
||||
send(liveShares)
|
||||
reschedule(liveShares)
|
||||
}
|
||||
}
|
||||
|
||||
collect { shares ->
|
||||
val liveShares = shares.filterLive()
|
||||
send(liveShares)
|
||||
reschedule(liveShares)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import io.element.android.libraries.architecture.AsyncData
|
|||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EmbeddedEventInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
|
||||
|
|
@ -20,6 +21,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
|
|
@ -34,8 +36,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause
|
|||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.poll.map
|
||||
import io.element.android.libraries.matrix.impl.room.join.map
|
||||
import io.element.android.libraries.matrix.impl.room.location.into
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import org.matrix.rustcomponents.sdk.BeaconInfo
|
||||
import org.matrix.rustcomponents.sdk.EmbeddedEventDetails
|
||||
import org.matrix.rustcomponents.sdk.MsgLikeContent
|
||||
import org.matrix.rustcomponents.sdk.MsgLikeKind
|
||||
|
|
@ -109,8 +113,14 @@ class TimelineEventContentMapper(
|
|||
)
|
||||
}
|
||||
is MsgLikeKind.LiveLocation -> {
|
||||
// Live location messages are a special kind of message that we want to treat as unknown content for now
|
||||
UnknownContent
|
||||
LiveLocationContent(
|
||||
isLive = kind.content.isLive,
|
||||
startTimestamp = kind.content.ts.toLong(),
|
||||
description = kind.content.description,
|
||||
timeout = kind.content.timeoutMs.toLong(),
|
||||
assetType = kind.content.assetType.into(),
|
||||
locations = kind.content.locations.map { location -> location.map() }
|
||||
)
|
||||
}
|
||||
is MsgLikeKind.Other -> UnknownContent
|
||||
}
|
||||
|
|
@ -266,3 +276,11 @@ private fun RustEncryptedMessage.map(): UnableToDecryptContent.Data {
|
|||
RustEncryptedMessage.Unknown -> UnableToDecryptContent.Data.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
private fun BeaconInfo.map(): LiveLocationInfo {
|
||||
return LiveLocationInfo(
|
||||
description = description,
|
||||
geoUri = geoUri,
|
||||
timestamp = ts.toLong(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.matrix.impl.room.location
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class TimedLiveLocationSharesFlowTest {
|
||||
@Test
|
||||
fun `it keeps emitting shares for subsequent expiries without upstream changes`() = runTest {
|
||||
val shares = listOf(
|
||||
aLiveLocationShare(userId = "@alice:server", endTimestamp = 1_000),
|
||||
aLiveLocationShare(userId = "@bob:server", endTimestamp = 2_000),
|
||||
aLiveLocationShare(userId = "@carol:server", endTimestamp = 3_000),
|
||||
)
|
||||
|
||||
flowOf(shares)
|
||||
.timedByExpiry(currentTimeMillis = { testScheduler.currentTime })
|
||||
.test {
|
||||
assertThat(awaitItem()).isEqualTo(shares)
|
||||
|
||||
advanceTimeBy(1_000)
|
||||
assertThat(awaitItem()).isEqualTo(shares.drop(1))
|
||||
|
||||
advanceTimeBy(999)
|
||||
expectNoEvents()
|
||||
|
||||
advanceTimeBy(1)
|
||||
assertThat(awaitItem()).isEqualTo(shares.drop(2))
|
||||
|
||||
advanceTimeBy(999)
|
||||
expectNoEvents()
|
||||
|
||||
advanceTimeBy(1)
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it does not double-emit when a share is already expired on receipt`() = runTest {
|
||||
val shares = listOf(
|
||||
aLiveLocationShare(userId = "@alice:server", endTimestamp = 500),
|
||||
aLiveLocationShare(userId = "@bob:server", endTimestamp = 2_000),
|
||||
)
|
||||
|
||||
flowOf(shares)
|
||||
.timedByExpiry(currentTimeMillis = { 1_000 + testScheduler.currentTime })
|
||||
.test {
|
||||
assertThat(awaitItem()).isEqualTo(shares.drop(1))
|
||||
expectNoEvents()
|
||||
|
||||
advanceTimeBy(999)
|
||||
expectNoEvents()
|
||||
|
||||
advanceTimeBy(1)
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it reschedules timed emission when upstream shares change`() = runTest {
|
||||
val upstream = MutableSharedFlow<List<LiveLocationShare>>(extraBufferCapacity = 1)
|
||||
val initialShares = listOf(aLiveLocationShare(endTimestamp = 10_000))
|
||||
val updatedShares = listOf(
|
||||
aLiveLocationShare(userId = "@alice:server", endTimestamp = 10_000),
|
||||
aLiveLocationShare(userId = "@bob:server", endTimestamp = 6_000),
|
||||
)
|
||||
|
||||
upstream
|
||||
.timedByExpiry(currentTimeMillis = { testScheduler.currentTime })
|
||||
.test {
|
||||
upstream.emit(initialShares)
|
||||
assertThat(awaitItem()).isEqualTo(initialShares)
|
||||
|
||||
advanceTimeBy(5_000)
|
||||
upstream.emit(updatedShares)
|
||||
assertThat(awaitItem()).isEqualTo(updatedShares)
|
||||
|
||||
advanceTimeBy(999)
|
||||
expectNoEvents()
|
||||
|
||||
advanceTimeBy(1)
|
||||
assertThat(awaitItem()).isEqualTo(updatedShares.take(1))
|
||||
|
||||
advanceTimeBy(3_999)
|
||||
expectNoEvents()
|
||||
|
||||
advanceTimeBy(1)
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it completes after the last scheduled re-emission when upstream completes`() = runTest {
|
||||
val shares = listOf(aLiveLocationShare(endTimestamp = 1_000))
|
||||
flowOf(shares)
|
||||
.timedByExpiry(currentTimeMillis = { testScheduler.currentTime })
|
||||
.test {
|
||||
assertThat(awaitItem()).isEqualTo(shares)
|
||||
|
||||
advanceTimeBy(1_000)
|
||||
assertThat(awaitItem()).isEmpty()
|
||||
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it completes immediately when upstream emits nothing`() = runTest {
|
||||
emptyFlow<List<LiveLocationShare>>()
|
||||
.timedByExpiry(currentTimeMillis = { testScheduler.currentTime })
|
||||
.test {
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun aLiveLocationShare(
|
||||
userId: String = "@user:server",
|
||||
endTimestamp: Long,
|
||||
): LiveLocationShare {
|
||||
return LiveLocationShare(
|
||||
userId = UserId(userId),
|
||||
lastLocation = null,
|
||||
startTimestamp = 0L,
|
||||
endTimestamp = endTimestamp,
|
||||
)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -34,51 +35,34 @@ import io.element.android.libraries.matrix.ui.model.getBestName
|
|||
|
||||
@Composable
|
||||
fun MatrixUserHeader(
|
||||
matrixUser: MatrixUser?,
|
||||
modifier: Modifier = Modifier,
|
||||
// TODO handle click on this item, to let the user be able to update their profile.
|
||||
// onClick: () -> Unit,
|
||||
) {
|
||||
if (matrixUser == null) {
|
||||
MatrixUserHeaderPlaceholder(modifier = modifier)
|
||||
} else {
|
||||
MatrixUserHeaderContent(
|
||||
matrixUser = matrixUser,
|
||||
modifier = modifier,
|
||||
// onClick = onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MatrixUserHeaderContent(
|
||||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
// onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
// .clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Avatar(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp),
|
||||
.padding(vertical = 7.dp),
|
||||
avatarData = matrixUser.getAvatarData(size = AvatarSize.UserPreference),
|
||||
avatarType = AvatarType.User,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Spacer(modifier = Modifier.width(13.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
text = matrixUser.getBestName(),
|
||||
maxLines = 1,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
style = ElementTheme.typography.fontHeadingMdRegular,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* 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.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.placeholderBackground
|
||||
|
||||
@Composable
|
||||
fun MatrixUserHeaderPlaceholder(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp)
|
||||
.size(AvatarSize.UserPreference.dp)
|
||||
.background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
PlaceholderAtom(width = 80.dp, height = 7.dp)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
PlaceholderAtom(width = 180.dp, height = 6.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MatrixUserHeaderPlaceholderPreview() = ElementPreview {
|
||||
MatrixUserHeaderPlaceholder()
|
||||
}
|
||||
|
|
@ -20,15 +20,6 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
|
|||
)
|
||||
}
|
||||
|
||||
open class MatrixUserWithNullProvider : PreviewParameterProvider<MatrixUser?> {
|
||||
override val values: Sequence<MatrixUser?>
|
||||
get() = sequenceOf(
|
||||
aMatrixUser(),
|
||||
aMatrixUser(displayName = null),
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
open class MatrixUserWithAvatarProvider : PreviewParameterProvider<MatrixUser?> {
|
||||
override val values: Sequence<MatrixUser?>
|
||||
get() = sequenceOf(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ package io.element.android.libraries.matrix.ui.components
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -23,12 +25,14 @@ fun MatrixUserRow(
|
|||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
avatarSize: AvatarSize = AvatarSize.UserListItem,
|
||||
verticalSpaceWidth: Dp = 12.dp,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
) = UserRow(
|
||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||
name = matrixUser.getBestName(),
|
||||
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||
modifier = modifier,
|
||||
verticalSpaceWidth = verticalSpaceWidth,
|
||||
trailingContent = trailingContent,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,16 @@ package io.element.android.libraries.matrix.ui.components
|
|||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
|
|
@ -31,22 +34,22 @@ internal fun UserRow(
|
|||
subtext: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
verticalSpaceWidth: Dp = 12.dp,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(verticalSpaceWidth))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.weight(1f),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Saada kutse"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"Kas sa soovid alustada vestlust kasutajaga %1$s?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Kas saadame kutse?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Kas alustad vestlust selle uue kontaktiga?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) saatis sulle kutse"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,7 @@
|
|||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Lähetä kutsu"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"Haluaisitko aloittaa keskustelun käyttäjän %1$s kanssa?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Lähetetäänkö kutsu?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"Sinulla ei ole tällä hetkellä keskusteluja tämän henkilön kanssa. Vahvista kutsu ennen jatkamista."</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Aloitetaanko keskustelu tämän uuden kontaktin kanssa?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) kutsui sinut"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,7 @@
|
|||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Envoyer l’invitation"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"Voulez-vous entamer une discussion avec %1$s ?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Envoyer l’invitation ?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"Vous n’avez actuellement aucune conversation avec cette personne. Confirmez son invitation avant de continuer."</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Entamer une conversation avec ce nouveau contact ?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) vous a invité(e)"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,7 @@
|
|||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Pošalji pozivnicu"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"Želite li započeti razgovor s korisnikom %1$s?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Želite li poslati pozivnicu?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"Trenutno nemate razgovora s ovom osobom. Potvrdite pozivanje prije nego što nastavite."</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Želite li započeti razgovor s ovim novim kontaktom?"</string>
|
||||
<string name="screen_invites_invited_you">"Pozvao vas je korisnik %1$s (%2$s)"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,7 @@
|
|||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Meghívó küldése"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"Csevegést kezd vele: %1$s?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Meghívó küldése?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"Még nem beszélgetett ezzel a személlyel. Folytatás előtt erősítse meg a meghívást."</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Csevegést kezdeményez ezzel az új felhasználóval?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) meghívta"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,7 @@
|
|||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Отправить приглашение"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"Хотите начать чат с %1$s?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Отправить приглашение?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"У Вас нет других чатов с этим пользователем. Подтвердите, что это действительно кого Вы хотите пригласить, прежде чем продолжить."</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Начать чат с этим новым контактом?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) пригласил(а) вас"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,7 @@
|
|||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"Taklif yuborish"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"%1$s bilan chatni boshlashni xohlaysizmi?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"Taklif yuborilsinmi?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"Ayni paytda bu shaxs bilan hech qanday suhbatingiz yo‘q. Davom etishdan oldin ularni taklif qilishni tasdiqlang."</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"Bu yangi kontakt bilan chat boshlansinmi?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s(%2$s ) sizni taklif qildi"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,7 @@
|
|||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"傳送邀請"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"您想要開始與 %1$s 聊天嗎?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"傳送邀請?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_content">"您目前與此人沒有任何聊天紀錄。請確認邀請後再繼續。"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"開始與這位新聯絡人聊天?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s(%2$s)邀請您"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
<string name="screen_bottom_sheet_create_dm_confirmation_button_title">"发送邀请"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_message">"您想与%1$s 开始聊天吗?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_title">"发送邀请?"</string>
|
||||
<string name="screen_bottom_sheet_create_dm_unknown_user_title">"是否与新联系人开始聊天?"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s)邀请了你"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -120,11 +120,25 @@ class MediaViewerDataSource(
|
|||
*/
|
||||
private fun buildMediaViewerPageList(groupedItems: List<MediaItem>) = buildList {
|
||||
// Filter out DateSeparator items, we do not need them for the media viewer
|
||||
val groupedItemsNoDateSeparator = groupedItems.filterNot { it is MediaItem.DateSeparator }
|
||||
pagerKeysHandler.accept(groupedItemsNoDateSeparator)
|
||||
groupedItemsNoDateSeparator.forEach { mediaItem ->
|
||||
val itemsNoDateSeparator = groupedItems.filterNot { it is MediaItem.DateSeparator }
|
||||
// Separate loading indicators and media events
|
||||
val loadingIndicators = itemsNoDateSeparator.filterIsInstance<MediaItem.LoadingIndicator>()
|
||||
val mediaEvents = itemsNoDateSeparator.filterIsInstance<MediaItem.Event>()
|
||||
// Determine backward and forward loading indicators
|
||||
val backwardLoading = loadingIndicators.find { it.direction == Timeline.PaginationDirection.BACKWARDS }
|
||||
val forwardLoading = loadingIndicators.find { it.direction == Timeline.PaginationDirection.FORWARDS }
|
||||
// Build ordered list: backward loading, media events (oldest first), forward loading
|
||||
// Media events are currently newest first, reverse to get oldest first
|
||||
val orderedEvents = mediaEvents.reversed()
|
||||
// Create new list of MediaItem in order: backwardLoading, orderedEvents, forwardLoading
|
||||
val orderedItems = buildList {
|
||||
backwardLoading?.let { add(it) }
|
||||
addAll(orderedEvents)
|
||||
forwardLoading?.let { add(it) }
|
||||
}
|
||||
pagerKeysHandler.accept(orderedItems)
|
||||
orderedItems.forEach { mediaItem ->
|
||||
when (mediaItem) {
|
||||
is MediaItem.DateSeparator -> Unit
|
||||
is MediaItem.Event -> {
|
||||
val sourceUrl = mediaItem.mediaSource().safeUrl
|
||||
val localMedia = localMediaStates.getOrPut(sourceUrl) {
|
||||
|
|
@ -148,6 +162,7 @@ class MediaViewerDataSource(
|
|||
pagerKey = pagerKeysHandler.getKey(mediaItem),
|
||||
)
|
||||
)
|
||||
is MediaItem.DateSeparator -> Unit // already filtered out
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
|
|
|||
|
|
@ -179,15 +179,19 @@ class MediaViewerPresenter(
|
|||
) {
|
||||
val isRenderingLoadingBackward by remember {
|
||||
derivedStateOf {
|
||||
currentIndex.intValue == data.value.lastIndex &&
|
||||
currentIndex.intValue == 0 &&
|
||||
data.value.size > 1 &&
|
||||
data.value.lastOrNull() is MediaViewerPageData.Loading
|
||||
data.value.firstOrNull() is MediaViewerPageData.Loading &&
|
||||
(data.value.firstOrNull() as? MediaViewerPageData.Loading)?.direction == Timeline.PaginationDirection.BACKWARDS
|
||||
}
|
||||
}
|
||||
if (isRenderingLoadingBackward) {
|
||||
LaunchedEffect(Unit) {
|
||||
// Observe the loading data vanishing
|
||||
snapshotFlow { data.value.lastOrNull() is MediaViewerPageData.Loading }
|
||||
snapshotFlow {
|
||||
val first = data.value.firstOrNull()
|
||||
first is MediaViewerPageData.Loading && first.direction == Timeline.PaginationDirection.BACKWARDS
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.filter { !it }
|
||||
.onEach { showNoMoreItemsSnackbar() }
|
||||
|
|
@ -203,15 +207,19 @@ class MediaViewerPresenter(
|
|||
) {
|
||||
val isRenderingLoadingForward by remember {
|
||||
derivedStateOf {
|
||||
currentIndex.intValue == 0 &&
|
||||
currentIndex.intValue == data.value.lastIndex &&
|
||||
data.value.size > 1 &&
|
||||
data.value.firstOrNull() is MediaViewerPageData.Loading
|
||||
data.value.lastOrNull() is MediaViewerPageData.Loading &&
|
||||
(data.value.lastOrNull() as? MediaViewerPageData.Loading)?.direction == Timeline.PaginationDirection.FORWARDS
|
||||
}
|
||||
}
|
||||
if (isRenderingLoadingForward) {
|
||||
LaunchedEffect(Unit) {
|
||||
// Observe the loading data vanishing
|
||||
snapshotFlow { data.value.firstOrNull() is MediaViewerPageData.Loading }
|
||||
snapshotFlow {
|
||||
val last = data.value.lastOrNull()
|
||||
last is MediaViewerPageData.Loading && last.direction == Timeline.PaginationDirection.FORWARDS
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.filter { !it }
|
||||
.onEach { showNoMoreItemsSnackbar() }
|
||||
|
|
|
|||
|
|
@ -593,20 +593,20 @@ class MediaViewerPresenterTest {
|
|||
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(),
|
||||
fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
|
||||
fileItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
|
||||
)
|
||||
} else {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
|
||||
imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
// User navigate to the first item (forward loading indicator)
|
||||
// User navigate to the last item (forward loading indicator)
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OnNavigateTo(0)
|
||||
MediaViewerEvents.OnNavigateTo(2)
|
||||
)
|
||||
// data source claims that there is no more items to load forward
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
|
|
@ -614,19 +614,21 @@ class MediaViewerPresenterTest {
|
|||
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(),
|
||||
fileItems = persistentListOf(anImage, aBackwardLoadingIndicator),
|
||||
fileItems = persistentListOf(aBackwardLoadingIndicator, anImage),
|
||||
)
|
||||
} else {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(anImage, aBackwardLoadingIndicator),
|
||||
imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
skipItems(1)
|
||||
val stateWithSnackbar = awaitItem()
|
||||
assertThat(stateWithSnackbar.snackbarMessage!!.messageResId).isEqualTo(expectedSnackbarResId)
|
||||
var stateWithSnackbar = awaitItem()
|
||||
while (stateWithSnackbar.snackbarMessage == null) {
|
||||
stateWithSnackbar = awaitItem()
|
||||
}
|
||||
assertThat(stateWithSnackbar.snackbarMessage.messageResId).isEqualTo(expectedSnackbarResId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -665,41 +667,42 @@ class MediaViewerPresenterTest {
|
|||
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(),
|
||||
fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
|
||||
fileItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
|
||||
)
|
||||
} else {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
|
||||
imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
val updatedState = awaitItem()
|
||||
// User navigate to the last item (backward loading indicator)
|
||||
// User navigate to the first item (backward loading indicator)
|
||||
updatedState.eventSink(
|
||||
MediaViewerEvents.OnNavigateTo(2)
|
||||
MediaViewerEvents.OnNavigateTo(0)
|
||||
)
|
||||
skipItems(1)
|
||||
// data source claims that there is no more items to load backward
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(),
|
||||
fileItems = persistentListOf(aForwardLoadingIndicator, anImage),
|
||||
fileItems = persistentListOf(anImage, aForwardLoadingIndicator),
|
||||
)
|
||||
} else {
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage),
|
||||
imageAndVideoItems = persistentListOf(anImage, aForwardLoadingIndicator),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
skipItems(1)
|
||||
val stateWithSnackbar = awaitItem()
|
||||
assertThat(stateWithSnackbar.snackbarMessage!!.messageResId).isEqualTo(expectedSnackbarResId)
|
||||
var stateWithSnackbar = awaitItem()
|
||||
while (stateWithSnackbar.snackbarMessage == null) {
|
||||
stateWithSnackbar = awaitItem()
|
||||
}
|
||||
assertThat(stateWithSnackbar.snackbarMessage.messageResId).isEqualTo(expectedSnackbarResId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -717,7 +720,7 @@ class MediaViewerPresenterTest {
|
|||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
GroupedMediaItems(
|
||||
imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
|
||||
imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
|
||||
fileItems = persistentListOf(),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.push.api.push
|
||||
|
||||
/**
|
||||
* A helper to manage the foreground service used to keep the device awake while we schedule and wait for the work to fetch the notification content to run.
|
||||
*/
|
||||
interface FetchPushForegroundServiceManager {
|
||||
/**
|
||||
* Start the foreground service to acquire the wakelock. If the device is already awake, this method does nothing.
|
||||
*
|
||||
* @return true if the service was started, false otherwise (e.g. if the device was already awake or if starting the service failed).
|
||||
*/
|
||||
fun start(): Boolean
|
||||
|
||||
/**
|
||||
* Stop the foreground service to release the wakelock. If the service is not running, this method does nothing.
|
||||
*
|
||||
* @return true if the service was stopped, false otherwise (e.g. if the service was not running or if stopping the service failed).
|
||||
*/
|
||||
suspend fun stop(): Boolean
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.push.api.push
|
||||
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Abstraction over wakelocks used for push handling to ensure the device stays awake while we handle the push and schedule and run the work.
|
||||
*/
|
||||
interface PushHandlingWakeLock {
|
||||
/**
|
||||
* Acquire a wakelock. The wakelock will be held for the given [time] or until [unlock] is called, whichever happens first.
|
||||
*/
|
||||
fun lock(time: Duration = 1.minutes)
|
||||
|
||||
/**
|
||||
* Release the wakelock. If no wakelock is associated with the key, this method does nothing.
|
||||
*/
|
||||
suspend fun unlock()
|
||||
}
|
||||
|
|
@ -13,8 +13,6 @@ import dev.zacsweers.metro.SingleIn
|
|||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
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.core.SessionId
|
||||
|
|
@ -32,7 +30,6 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
|
|||
import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore
|
||||
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
|
||||
import io.element.android.libraries.push.impl.push.OnRedactedEventReceived
|
||||
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
|
||||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -60,8 +57,6 @@ class DefaultNotificationResultProcessor(
|
|||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val onRedactedEventReceived: OnRedactedEventReceived,
|
||||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
|
||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
@AppCoroutineScope private val coroutineScope: CoroutineScope,
|
||||
|
|
@ -215,10 +210,6 @@ class DefaultNotificationResultProcessor(
|
|||
if (nonRingingCallEvents.isNotEmpty()) {
|
||||
onNotifiableEventReceived.onNotifiableEventsReceived(nonRingingCallEvents)
|
||||
}
|
||||
|
||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncNotificationsWithWorkManager)) {
|
||||
syncOnNotifiableEvent(results.keys.toList())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.push.impl.push
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Context.ACTIVITY_SERVICE
|
||||
import android.content.Context.POWER_SERVICE
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultFetchPushForegroundServiceManager(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : FetchPushForegroundServiceManager {
|
||||
private val stopMutex = Mutex()
|
||||
|
||||
override fun start(): Boolean {
|
||||
Timber.d("Acquiring wakelock for push handling, starting service.")
|
||||
|
||||
// Don't start the foreground service if the device is already awake
|
||||
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||
if (powerManager.isInteractive) {
|
||||
Timber.d("Device is already in an interactive state, no need to start FetchPushForegroundService")
|
||||
return false
|
||||
}
|
||||
|
||||
val intent = Intent(context, FetchPushForegroundService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
runCatchingExceptions { ContextCompat.startForegroundService(context, intent) }
|
||||
.onFailure { throwable ->
|
||||
Timber.e(throwable, "Failed to start FetchPushForegroundService, notifications may take longer than usual to sync")
|
||||
}
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun stop(): Boolean {
|
||||
Timber.d("Releasing wakelock used for push handling, stopping service.")
|
||||
return stopMutex.withLock {
|
||||
val runningServiceInfo = getRunningServiceInfo(context)
|
||||
if (runningServiceInfo != null) {
|
||||
val intent = Intent(context, FetchPushForegroundService::class.java)
|
||||
// If it's still not running in foreground, it means the service is still starting,
|
||||
// so we delay the stop to give it time to start and be set as foreground, otherwise we can crash
|
||||
// with `ForegroundServiceDidNotStartInTimeException`.
|
||||
var isInForeground = runningServiceInfo.foreground
|
||||
withTimeoutOrNull(5.seconds) {
|
||||
while (!isInForeground) {
|
||||
delay(50)
|
||||
val updatedServiceInfo = getRunningServiceInfo(context)
|
||||
if (updatedServiceInfo == null) {
|
||||
Timber.d("FetchPushForegroundService is no longer running, no need to stop it.")
|
||||
return@withTimeoutOrNull
|
||||
}
|
||||
isInForeground = updatedServiceInfo.foreground == true
|
||||
}
|
||||
} ?: Timber.w("FetchPushForegroundService did not start in foreground after 5s, stopping it anyway.")
|
||||
context.stopService(intent)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getRunningServiceInfo(context: Context): ActivityManager.RunningServiceInfo? {
|
||||
val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||
return activityManager.getRunningServices(Int.MAX_VALUE)
|
||||
.firstOrNull { it.service.className == FetchPushForegroundService::class.java.name }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.push.impl.push
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultPushHandlingWakeLock(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : PushHandlingWakeLock {
|
||||
override fun lock(time: Duration) {
|
||||
Timber.d("Acquiring wakelock for push handling, starting service.")
|
||||
FetchPushForegroundService.startIfNeeded(context)
|
||||
}
|
||||
|
||||
override suspend fun unlock() {
|
||||
Timber.d("Releasing wakelock used for push handling.")
|
||||
FetchPushForegroundService.stop(context)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,32 +7,27 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.push
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||
import io.element.android.libraries.push.impl.di.PushBindings
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
|
||||
|
|
@ -48,7 +43,6 @@ class FetchPushForegroundService : Service() {
|
|||
}
|
||||
|
||||
@Inject lateinit var notificationChannels: NotificationChannels
|
||||
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
|
||||
@Inject @AppCoroutineScope lateinit var coroutineScope: CoroutineScope
|
||||
|
||||
private val wakelock: PowerManager.WakeLock by lazy {
|
||||
|
|
@ -78,8 +72,13 @@ class FetchPushForegroundService : Service() {
|
|||
// Try to start the service in foreground. This can fail, even in cases where it's supposed to work according to the docs.
|
||||
// In those cases we catch the exception and handle the failure so we don't try to start the wakelock or stop the service
|
||||
// from running in foreground later.
|
||||
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
runCatchingExceptions {
|
||||
startForeground(NOTIFICATION_ID, notificationCompat)
|
||||
ServiceCompat.startForeground(this, NOTIFICATION_ID, notificationCompat, serviceType)
|
||||
}
|
||||
.onSuccess {
|
||||
isOnForeground = true
|
||||
|
|
@ -116,7 +115,7 @@ class FetchPushForegroundService : Service() {
|
|||
override fun stopService(intent: Intent?): Boolean {
|
||||
if (isOnForeground) {
|
||||
wakelock.release()
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
|
||||
return super.stopService(intent)
|
||||
|
|
@ -131,64 +130,7 @@ class FetchPushForegroundService : Service() {
|
|||
Timber.d("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground")
|
||||
if (isOnForeground) {
|
||||
Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService")
|
||||
coroutineScope.launch { pushHandlingWakeLock.unlock() }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val stopMutex = Mutex()
|
||||
|
||||
fun startIfNeeded(context: Context) {
|
||||
// Don't start the foreground service if the device is already awake
|
||||
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||
if (powerManager.isInteractive) return
|
||||
|
||||
start(context)
|
||||
}
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, FetchPushForegroundService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
runCatchingExceptions { context.startForegroundService(intent) }
|
||||
.onFailure { throwable ->
|
||||
Timber.e(
|
||||
throwable,
|
||||
"Failed to start FetchPushForegroundService, notifications may take longer than usual to sync"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stop(context: Context) = stopMutex.withLock {
|
||||
val runningServiceInfo = getRunningServiceInfo(context)
|
||||
if (runningServiceInfo != null) {
|
||||
val intent = Intent(context, FetchPushForegroundService::class.java)
|
||||
// If it's still not running in foreground, it means the service is still starting,
|
||||
// so we delay the stop to give it time to start and be set as foreground, otherwise we can crash
|
||||
// with `ForegroundServiceDidNotStartInTimeException`.
|
||||
var isInForeground = runningServiceInfo.foreground
|
||||
withTimeoutOrNull(5.seconds) {
|
||||
while (!isInForeground) {
|
||||
delay(50)
|
||||
val updatedServiceInfo = getRunningServiceInfo(context)
|
||||
if (updatedServiceInfo == null) {
|
||||
Timber.d("FetchPushForegroundService is no longer running, no need to stop it.")
|
||||
return@withTimeoutOrNull
|
||||
}
|
||||
isInForeground = updatedServiceInfo.foreground == true
|
||||
}
|
||||
} ?: Timber.w("FetchPushForegroundService did not start in foreground after 5s, stopping it anyway.")
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getRunningServiceInfo(context: Context): ActivityManager.RunningServiceInfo? {
|
||||
val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||
return activityManager.getRunningServices(Int.MAX_VALUE)
|
||||
.firstOrNull { it.service.className == FetchPushForegroundService::class.java.name }
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.auth.SessionRestorationException
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
import io.element.android.libraries.matrix.api.exception.isNetworkError
|
||||
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||
import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
|
||||
import io.element.android.libraries.push.impl.db.PushRequest
|
||||
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||
|
|
@ -58,7 +58,7 @@ class FetchPendingNotificationsWorker(
|
|||
private val resultProcessor: NotificationResultProcessor,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val systemClock: SystemClock,
|
||||
private val pushHandlingWakeLock: PushHandlingWakeLock,
|
||||
private val fetchPushForegroundServiceManager: FetchPushForegroundServiceManager,
|
||||
) : CoroutineWorker(context, params) {
|
||||
override suspend fun doWork(): Result {
|
||||
Timber.d("FetchNotificationsWorker started")
|
||||
|
|
@ -67,7 +67,8 @@ class FetchPendingNotificationsWorker(
|
|||
inputData.getString(SyncPendingNotificationsRequestBuilder.SESSION_ID)?.let(::SessionId)
|
||||
}.getOrNull() ?: return Result.failure()
|
||||
|
||||
pushHandlingWakeLock.unlock()
|
||||
// We can stop the foreground service and unlock the wakelock, since the work is now running and the device should be kept awake
|
||||
fetchPushForegroundServiceManager.stop()
|
||||
|
||||
// Fetch pending requests in the last 24 hours
|
||||
val fetchSince = Instant.fromEpochMilliseconds(systemClock.epochMillis()).minus(1.days)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@
|
|||
</plurals>
|
||||
<string name="notification_error_unified_push_unregistered_android">"Der Dienst für UnifiedPush Benachrichtigungen konnte nicht registriert werden. Daher können aktuell keine Push-Benachrichtigungen erhalten werden. Bitte überprüfe die Einstellungen der Benachrichtigungen in der App und den Status des Push-Dienstes."</string>
|
||||
<string name="notification_fallback_content">"Du hast neue Nachrichten."</string>
|
||||
<plurals name="notification_fallback_n_content">
|
||||
<item quantity="one">"Du hast %d neue Nachricht."</item>
|
||||
<item quantity="other">"Du hast %d neue Nachrichten."</item>
|
||||
</plurals>
|
||||
<string name="notification_incoming_audio_call">"📞 Eingehender Anruf"</string>
|
||||
<string name="notification_incoming_call">"Eingehender Anruf"</string>
|
||||
<string name="notification_inline_reply_failed">"** Fehler beim Senden - bitte Chat öffnen"</string>
|
||||
<string name="notification_invitation_action_join">"Beitreten"</string>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@
|
|||
</plurals>
|
||||
<string name="notification_error_unified_push_unregistered_android">"Distributer obavijesti UnifiedPush nije mogao biti registriran, tako da više nećete primati obavijesti. Provjerite postavke obavijesti u aplikaciji i status distributera push obavijesti."</string>
|
||||
<string name="notification_fallback_content">"Imate nove poruke."</string>
|
||||
<plurals name="notification_fallback_n_content">
|
||||
<item quantity="one">"Imate%d novu poruku."</item>
|
||||
<item quantity="other">"Imate %d novi poruka."</item>
|
||||
</plurals>
|
||||
<string name="notification_incoming_audio_call">"📞 Dolazni poziv"</string>
|
||||
<string name="notification_incoming_call">"📹 Dolazni poziv"</string>
|
||||
<string name="notification_inline_reply_failed">"** Slanje nije uspjelo – otvorite sobu"</string>
|
||||
<string name="notification_invitation_action_join">"Pridruži se"</string>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@
|
|||
</plurals>
|
||||
<string name="notification_error_unified_push_unregistered_android">"UnifiedPush bildirishnoma tarqatuvchisini roʻyxatdan oʻtkazib boʻlmadi, shuning uchun siz endi bildirishnomalarni olmaysiz. Iltimos, ilovaning bildirishnoma sozlamalarini va push distribyutor holatini tekshiring."</string>
|
||||
<string name="notification_fallback_content">"Sizda yangi xabarlar bor."</string>
|
||||
<plurals name="notification_fallback_n_content">
|
||||
<item quantity="one">"Sizda %d ta yangi xabar bor."</item>
|
||||
<item quantity="other">"Sizda %d ta yangi xabar bor."</item>
|
||||
</plurals>
|
||||
<string name="notification_incoming_audio_call">"📞 Kiruvchi qo‘ng‘iroq"</string>
|
||||
<string name="notification_incoming_call">"📹 Kiruvchi qoʻngʻiroq"</string>
|
||||
<string name="notification_inline_reply_failed">"** Yuborilmadi - iltimos, xonani oching"</string>
|
||||
<string name="notification_invitation_action_join">"Qo\'shilish"</string>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@
|
|||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="other">"%d thông báo"</item>
|
||||
</plurals>
|
||||
<string name="notification_error_unified_push_unregistered_android">"Không thể đăng ký trình phân phối thông báo UnifiedPush, vì vậy bạn sẽ không nhận được thông báo nữa. Vui lòng kiểm tra cài đặt thông báo của ứng dụng và trạng thái của trình phân phối thông báo."</string>
|
||||
<string name="notification_fallback_content">"Bạn có tin nhắn mới."</string>
|
||||
<plurals name="notification_fallback_n_content">
|
||||
<item quantity="other">"Bạn có %d tin nhắn mới."</item>
|
||||
</plurals>
|
||||
<string name="notification_incoming_audio_call">"📞 Cuộc gọi đến"</string>
|
||||
<string name="notification_incoming_call">"📹 Cuộc gọi đến"</string>
|
||||
<string name="notification_inline_reply_failed">"** Không gửi được - vui lòng mở phòng"</string>
|
||||
<string name="notification_invitation_action_join">"Tham gia"</string>
|
||||
|
|
@ -20,6 +25,7 @@
|
|||
<item quantity="other">"%d lời mời"</item>
|
||||
</plurals>
|
||||
<string name="notification_invite_body">"Đã mời bạn trò chuyện"</string>
|
||||
<string name="notification_invite_body_with_sender">"%1$s đã mời bạn trò chuyện"</string>
|
||||
<string name="notification_mentioned_you_body">"Đã nhắc đến bạn: %1$s"</string>
|
||||
<string name="notification_new_messages">"Tin nhắn mới"</string>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
|
|
@ -29,8 +35,13 @@
|
|||
<string name="notification_room_action_mark_as_read">"Đánh dấu đã đọc"</string>
|
||||
<string name="notification_room_action_quick_reply">"Trả lời nhanh"</string>
|
||||
<string name="notification_room_invite_body">"Đã mời bạn tham gia phòng"</string>
|
||||
<string name="notification_room_invite_body_with_sender">"%1$s đã mời bạn tham gia phòng chat"</string>
|
||||
<string name="notification_sender_me">"Tôi"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s đã đề cập hoặc trả lời"</string>
|
||||
<string name="notification_space_invite_body">"Mời bạn tham gia không gian này."</string>
|
||||
<string name="notification_space_invite_body_with_sender">"%1$s đã mời bạn tham gia không gian này."</string>
|
||||
<string name="notification_test_push_notification_content">"Bạn đang xem thông báo! Bấm vào đây!"</string>
|
||||
<string name="notification_thread_in_room">"Chuỗi cuộc trò chuyện trong: %1$s"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s:%2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
|
|
@ -45,6 +56,9 @@
|
|||
<string name="push_distributor_background_sync_android">"Đồng bộ hóa trong nền"</string>
|
||||
<string name="push_distributor_firebase_android">"Dịch vụ của Google"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Không tìm thấy Dịch vụ Google Play hợp lệ. Thông báo có thể không hoạt động đúng cách."</string>
|
||||
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
|
||||
<item quantity="other">"Bạn đã chặn người dùng %1$d. Bạn sẽ không nhận được thông báo từ người này."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_blocked_users_title">"Người dùng bị chặn"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Hãy đảm bảo rằng ứng dụng hỗ trợ ít nhất một nhà cung cấp thông báo đẩy."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Không tìm thấy hỗ trợ từ nhà cung cấp thông báo đẩy."</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@
|
|||
</plurals>
|
||||
<string name="notification_error_unified_push_unregistered_android">"Unified Push 通知散佈程式註冊失敗,因此您無法再收到通知。請檢查應用程式的通知設定與推播散佈程式的狀態。"</string>
|
||||
<string name="notification_fallback_content">"您有新訊息。"</string>
|
||||
<plurals name="notification_fallback_n_content">
|
||||
<item quantity="other">"您有 %d 則新訊息。"</item>
|
||||
</plurals>
|
||||
<string name="notification_incoming_audio_call">"📞 來電"</string>
|
||||
<string name="notification_incoming_call">"📹 來電"</string>
|
||||
<string name="notification_inline_reply_failed">"** 無法傳送,請開啟聊天室"</string>
|
||||
<string name="notification_invitation_action_join">"加入"</string>
|
||||
|
|
@ -34,6 +38,8 @@
|
|||
<string name="notification_room_invite_body_with_sender">"%1$s 邀請您加入聊天室"</string>
|
||||
<string name="notification_sender_me">"我"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s 提及或回覆"</string>
|
||||
<string name="notification_space_invite_body">"已邀請您加入空間"</string>
|
||||
<string name="notification_space_invite_body_with_sender">"%1$s 已邀請您的加入此空間"</string>
|
||||
<string name="notification_test_push_notification_content">"您正在查看通知!點我!"</string>
|
||||
<string name="notification_thread_in_room">"在 %1$s 的討論串"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s:%2$s"</string>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.test.FakeElementCallEntryPoint
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
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.core.SessionId
|
||||
|
|
@ -34,7 +33,6 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
|
|||
import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore
|
||||
import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived
|
||||
import io.element.android.libraries.push.impl.push.FakeOnRedactedEventReceived
|
||||
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
|
||||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
|
|
@ -289,8 +287,6 @@ class DefaultNotificationResultProcessorTest {
|
|||
userPushStoreFactory: FakeUserPushStoreFactory = FakeUserPushStoreFactory(),
|
||||
onRedactedEventReceived: (List<ResolvedPushEvent.Redaction>) -> Unit = {},
|
||||
onNotifiableEventsReceived: (List<NotifiableEvent>) -> Unit = {},
|
||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
||||
syncOnNotifiableEvent: SyncOnNotifiableEvent = {},
|
||||
elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
|
||||
notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
|
||||
coroutineScope: CoroutineScope = backgroundScope,
|
||||
|
|
@ -301,8 +297,6 @@ class DefaultNotificationResultProcessorTest {
|
|||
userPushStoreFactory = userPushStoreFactory,
|
||||
onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived),
|
||||
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived),
|
||||
featureFlagService = featureFlagService,
|
||||
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notificationChannels = notificationChannels,
|
||||
coroutineScope = coroutineScope,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.push.impl.push
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context.ACTIVITY_SERVICE
|
||||
import android.content.Context.POWER_SERVICE
|
||||
import android.os.PowerManager
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Shadows
|
||||
import org.robolectric.shadows.ShadowActivityManager
|
||||
import org.robolectric.shadows.ShadowPowerManager
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DefaultFetchPushForegroundServiceManagerTest {
|
||||
@Test
|
||||
fun `start should start the service if the device is not interactive`() {
|
||||
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||
|
||||
getShadowPowerManager().turnScreenOn(false)
|
||||
|
||||
assertThat(manager.start()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `start won't start the service if the device is interactive`() {
|
||||
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||
|
||||
getShadowPowerManager().turnScreenOn(true)
|
||||
|
||||
assertThat(manager.start()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stop will stop the service if it's running`() = runTest {
|
||||
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||
|
||||
// Start the service first
|
||||
getShadowPowerManager().turnScreenOn(false)
|
||||
manager.start()
|
||||
|
||||
getShadowActivityManager().setServices(
|
||||
listOf(
|
||||
ActivityManager.RunningServiceInfo().apply {
|
||||
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
|
||||
foreground = true
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat(manager.stop()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stop will eventually stop the service once it's on foreground`() = runTest {
|
||||
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||
|
||||
// Start the service first
|
||||
getShadowPowerManager().turnScreenOn(false)
|
||||
manager.start()
|
||||
|
||||
// The service is started, but not yet in foreground
|
||||
getShadowActivityManager().setServices(
|
||||
listOf(
|
||||
ActivityManager.RunningServiceInfo().apply {
|
||||
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
|
||||
foreground = false
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// We call stop, which won't stop the service yet since it's not in foreground
|
||||
val future = async { manager.stop() }
|
||||
|
||||
// Then we set the service as running in foreground, which should allow the stop to complete
|
||||
getShadowActivityManager().setServices(
|
||||
listOf(
|
||||
ActivityManager.RunningServiceInfo().apply {
|
||||
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
|
||||
foreground = true
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val stopped = withTimeout(5.seconds) { future.await() }
|
||||
assertThat(stopped).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stop will not stop the service if it's stopped`() = runTest {
|
||||
val manager = createDefaultFetchPushForegroundServiceManager()
|
||||
|
||||
// Set some fake running service data, even if the service is not really running
|
||||
getShadowActivityManager().setServices(
|
||||
listOf(
|
||||
ActivityManager.RunningServiceInfo().apply {
|
||||
service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java)
|
||||
foreground = true
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Since the service was not really running, it was not stopped
|
||||
assertThat(manager.stop()).isFalse()
|
||||
}
|
||||
|
||||
private fun createDefaultFetchPushForegroundServiceManager() = DefaultFetchPushForegroundServiceManager(
|
||||
context = InstrumentationRegistry.getInstrumentation().context,
|
||||
)
|
||||
|
||||
private fun getShadowPowerManager(): ShadowPowerManager {
|
||||
val powerManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(POWER_SERVICE) as PowerManager
|
||||
return Shadows.shadowOf(powerManager)
|
||||
}
|
||||
|
||||
private fun getShadowActivityManager(): ShadowActivityManager {
|
||||
val activityManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||
return Shadows.shadowOf(activityManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ import io.element.android.libraries.push.impl.notifications.FakeNotificationResu
|
|||
import io.element.android.libraries.push.impl.notifications.fixtures.aPushRequest
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent
|
||||
import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock
|
||||
import io.element.android.libraries.push.test.push.FakeFetchPushForegroundServiceManager
|
||||
import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
|
||||
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
|
|
@ -239,7 +239,7 @@ class FetchPendingNotificationWorkerTest {
|
|||
pushHistoryService: FakePushHistoryService = FakePushHistoryService(),
|
||||
resultProcessor: FakeNotificationResultProcessor = FakeNotificationResultProcessor(),
|
||||
systemClock: FakeSystemClock = FakeSystemClock(),
|
||||
pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(),
|
||||
pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
|
||||
) = FetchPendingNotificationsWorker(
|
||||
params = createWorkerParams(workDataOf("session_id" to input)),
|
||||
context = InstrumentationRegistry.getInstrumentation().context,
|
||||
|
|
@ -250,7 +250,7 @@ class FetchPendingNotificationWorkerTest {
|
|||
pushHistoryService = pushHistoryService,
|
||||
resultProcessor = resultProcessor,
|
||||
systemClock = systemClock,
|
||||
pushHandlingWakeLock = pushHandlingWakeLock,
|
||||
fetchPushForegroundServiceManager = pushHandlingWakeLock,
|
||||
)
|
||||
|
||||
private fun TestScope.createWorkerParams(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.push.test.push
|
||||
|
||||
import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
|
||||
|
||||
class FakeFetchPushForegroundServiceManager(
|
||||
private val lock: () -> Boolean = { true },
|
||||
private val unlock: () -> Boolean = { true },
|
||||
) : FetchPushForegroundServiceManager {
|
||||
override fun start(): Boolean {
|
||||
return lock.invoke()
|
||||
}
|
||||
|
||||
override suspend fun stop(): Boolean {
|
||||
return unlock.invoke()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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.libraries.push.test.push
|
||||
|
||||
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||
import kotlin.time.Duration
|
||||
|
||||
class FakePushHandlingWakeLock(
|
||||
private val lock: (time: Duration) -> Unit = {},
|
||||
private val unlock: () -> Unit = {},
|
||||
) : PushHandlingWakeLock {
|
||||
override fun lock(time: Duration) {
|
||||
lock.invoke(time)
|
||||
}
|
||||
|
||||
override suspend fun unlock() {
|
||||
unlock.invoke()
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import dev.zacsweers.metro.Inject
|
|||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||
import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
|
||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -27,7 +27,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
|
||||
@Inject lateinit var pushParser: FirebasePushParser
|
||||
@Inject lateinit var pushHandler: PushHandler
|
||||
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
|
||||
@Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager
|
||||
@AppCoroutineScope
|
||||
@Inject lateinit var coroutineScope: CoroutineScope
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||
val isHighPriority = message.priority == PRIORITY_HIGH
|
||||
if (isHighPriority) {
|
||||
// Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work
|
||||
pushHandlingWakeLock.lock()
|
||||
fetchPushForegroundServiceManager.start()
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
|
|
@ -63,7 +63,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||
},
|
||||
)
|
||||
if (isHighPriority) {
|
||||
pushHandlingWakeLock.unlock()
|
||||
fetchPushForegroundServiceManager.stop()
|
||||
}
|
||||
} else {
|
||||
val handled = pushHandler.handle(
|
||||
|
|
@ -73,7 +73,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||
|
||||
// If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long.
|
||||
if (!handled && isHighPriority) {
|
||||
pushHandlingWakeLock.unlock()
|
||||
fetchPushForegroundServiceManager.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.google.firebase.messaging.RemoteMessage
|
|||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SECRET
|
||||
import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock
|
||||
import io.element.android.libraries.push.test.push.FakeFetchPushForegroundServiceManager
|
||||
import io.element.android.libraries.push.test.test.FakePushHandler
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
|
|
@ -29,7 +29,6 @@ import kotlinx.coroutines.test.runTest
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import kotlin.time.Duration
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class VectorFirebaseMessagingServiceTest {
|
||||
|
|
@ -81,11 +80,11 @@ class VectorFirebaseMessagingServiceTest {
|
|||
|
||||
@Test
|
||||
fun `test pushHandler returning true locks and does not unlock the wakelock so it continues running`() = runTest {
|
||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
||||
val unlockLambda = lambdaRecorder<Unit> { }
|
||||
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||
pushHandler = FakePushHandler(handleResult = { _, _ -> true }),
|
||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
||||
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||
lock = lockLambda,
|
||||
unlock = unlockLambda
|
||||
)
|
||||
|
|
@ -113,11 +112,11 @@ class VectorFirebaseMessagingServiceTest {
|
|||
|
||||
@Test
|
||||
fun `test pushHandler returning false locks and unlocks the wakelock early`() = runTest {
|
||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
||||
val unlockLambda = lambdaRecorder<Unit> { }
|
||||
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||
pushHandler = FakePushHandler(handleResult = { _, _ -> false }),
|
||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
||||
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||
lock = lockLambda,
|
||||
unlock = unlockLambda
|
||||
)
|
||||
|
|
@ -145,11 +144,11 @@ class VectorFirebaseMessagingServiceTest {
|
|||
|
||||
@Test
|
||||
fun `test pushHandler with a remote message with normal priority won't lock the wakelock`() = runTest {
|
||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
||||
val unlockLambda = lambdaRecorder<Unit> { }
|
||||
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||
pushHandler = FakePushHandler(handleResult = { _, _ -> false }),
|
||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
||||
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||
lock = lockLambda,
|
||||
unlock = unlockLambda
|
||||
)
|
||||
|
|
@ -186,14 +185,14 @@ class VectorFirebaseMessagingServiceTest {
|
|||
private fun TestScope.createVectorFirebaseMessagingService(
|
||||
firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(),
|
||||
pushHandler: PushHandler = FakePushHandler(),
|
||||
pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(),
|
||||
pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
|
||||
): VectorFirebaseMessagingService {
|
||||
return VectorFirebaseMessagingService().apply {
|
||||
this.firebaseNewTokenHandler = firebaseNewTokenHandler
|
||||
this.pushParser = FirebasePushParser()
|
||||
this.pushHandler = pushHandler
|
||||
this.coroutineScope = this@createVectorFirebaseMessagingService
|
||||
this.pushHandlingWakeLock = pushHandlingWakeLock
|
||||
this.fetchPushForegroundServiceManager = pushHandlingWakeLock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import dev.zacsweers.metro.Inject
|
|||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.push.api.push.PushHandlingWakeLock
|
||||
import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager
|
||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
|
||||
|
|
@ -38,7 +38,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
|
||||
@Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler
|
||||
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
|
||||
@Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock
|
||||
@Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager
|
||||
|
||||
@AppCoroutineScope
|
||||
@Inject lateinit var coroutineScope: CoroutineScope
|
||||
|
|
@ -59,8 +59,8 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||
* @param instance connection, for multi-account
|
||||
*/
|
||||
override fun onMessage(context: Context, message: PushMessage, instance: String) {
|
||||
// Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work
|
||||
pushHandlingWakeLock.lock()
|
||||
// Start the foreground service to ensure the device stays awake while we handle the push and schedule and run the work.
|
||||
fetchPushForegroundServiceManager.start()
|
||||
|
||||
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
|
||||
coroutineScope.launch {
|
||||
|
|
@ -71,16 +71,16 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||
data = String(message.content),
|
||||
)
|
||||
pushHandlingWakeLock.unlock()
|
||||
fetchPushForegroundServiceManager.stop()
|
||||
} else {
|
||||
val handled = pushHandler.handle(
|
||||
pushData = pushData,
|
||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||
)
|
||||
|
||||
// If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long.
|
||||
// If we failed to handle the push, we should stop the foreground service early to avoid keeping the device awake for too long.
|
||||
if (!handled) {
|
||||
pushHandlingWakeLock.unlock()
|
||||
fetchPushForegroundServiceManager.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="other">"%1$d nhà phân phối đã tìm thấy: %2$s ."</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
|||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SECRET
|
||||
import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock
|
||||
import io.element.android.libraries.push.test.push.FakeFetchPushForegroundServiceManager
|
||||
import io.element.android.libraries.push.test.test.FakePushHandler
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
|
|
@ -39,7 +39,6 @@ import org.unifiedpush.android.connector.FailedReason
|
|||
import org.unifiedpush.android.connector.data.PublicKeySet
|
||||
import org.unifiedpush.android.connector.data.PushEndpoint
|
||||
import org.unifiedpush.android.connector.data.PushMessage
|
||||
import kotlin.time.Duration
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class VectorUnifiedPushMessagingReceiverTest {
|
||||
|
|
@ -106,13 +105,13 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
fun `pushHandler returning true locks the wake lock but does not unlock it so it continues to run`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> true }
|
||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
||||
val unlockLambda = lambdaRecorder<Unit> { }
|
||||
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||
pushHandler = FakePushHandler(
|
||||
handleResult = pushHandlerResult
|
||||
),
|
||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
||||
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||
lock = lockLambda,
|
||||
unlock = unlockLambda,
|
||||
),
|
||||
|
|
@ -133,13 +132,13 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
fun `pushHandler returning false locks and unlocks the wakelock early`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val pushHandlerResult = lambdaRecorder<PushData, String, Boolean> { _, _ -> false }
|
||||
val lockLambda = lambdaRecorder<Duration, Unit> { _ -> }
|
||||
val unlockLambda = lambdaRecorder<Unit> { }
|
||||
val lockLambda = lambdaRecorder<Boolean> { true }
|
||||
val unlockLambda = lambdaRecorder<Boolean> { true }
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||
pushHandler = FakePushHandler(
|
||||
handleResult = pushHandlerResult
|
||||
),
|
||||
pushHandlingWakeLock = FakePushHandlingWakeLock(
|
||||
pushHandlingWakeLock = FakeFetchPushForegroundServiceManager(
|
||||
lock = lockLambda,
|
||||
unlock = unlockLambda,
|
||||
),
|
||||
|
|
@ -264,7 +263,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(),
|
||||
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
|
||||
removedGatewayHandler: UnifiedPushRemovedGatewayHandler = UnifiedPushRemovedGatewayHandler { lambdaError() },
|
||||
pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(),
|
||||
pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(),
|
||||
): VectorUnifiedPushMessagingReceiver {
|
||||
return VectorUnifiedPushMessagingReceiver().apply {
|
||||
this.pushParser = unifiedPushParser
|
||||
|
|
@ -277,7 +276,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
this.removedGatewayHandler = removedGatewayHandler
|
||||
this.endpointRegistrationHandler = endpointRegistrationHandler
|
||||
this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver
|
||||
this.pushHandlingWakeLock = pushHandlingWakeLock
|
||||
this.fetchPushForegroundServiceManager = pushHandlingWakeLock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
libraries/rustls-tls/README.md
Normal file
9
libraries/rustls-tls/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
This module is a wrapper for the Android code distributed in the rustls-platform-verifier-android crate.
|
||||
|
||||
To avoid the distribution mess that this library has (download a Rust crate, then search for it using Gradle and use it as local maven repo),
|
||||
we previously just manually updated the AAR file instead using a script. This won't work for F-Droid because the AAR library is a black box with
|
||||
no sources attached to it, so we can't use it like that.
|
||||
|
||||
Instead, for the time being, we're adding the single `CertificateVerifier.kt` class this AAR had in it as part of our sources.
|
||||
|
||||
When this file is updated, the [UPDATED.md](./UPDATED.md) file should be updated too with the commit SHA of the new version.
|
||||
7
libraries/rustls-tls/UPDATED.md
Normal file
7
libraries/rustls-tls/UPDATED.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
Below is the commit SHA in [rustls-platform-verifier](https://github.com/rustls/rustls-platform-verifier) library used to update the code in this module:
|
||||
|
||||
```
|
||||
996b1c903491641b17b3c9afb65d1352f6fc6b76
|
||||
```
|
||||
|
||||
Please update it after making manual changes.
|
||||
24
libraries/rustls-tls/build.gradle.kts
Normal file
24
libraries/rustls-tls/build.gradle.kts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import extension.buildConfigFieldBoolean
|
||||
|
||||
/*
|
||||
* Copyright (c) 2026 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.rustls.platformverifier"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
buildConfigFieldBoolean("TEST", false)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,480 @@
|
|||
@file:SuppressLint("LogNotTimber", "ObsoleteSdkInt")
|
||||
@file:Suppress("KotlinConstantConditions")
|
||||
|
||||
// IMPORTANT: this file comes from rustls-platform-verifier and should not be modified locally.
|
||||
|
||||
/*
|
||||
* Copyright (c) 2022 1Password
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
package org.rustls.platformverifier
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.http.X509TrustManagerExtensions
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.MessageDigest
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPathValidator
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateExpiredException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.CertificateNotYetValidException
|
||||
import java.security.cert.CertificateParsingException
|
||||
import java.security.cert.PKIXBuilderParameters
|
||||
import java.security.cert.PKIXRevocationChecker
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Date
|
||||
import java.util.EnumSet
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
// If this is updated, update the Rust definition too.
|
||||
// Marked private as this is not meant to be used in Android code.
|
||||
private enum class StatusCode(val value: Int) {
|
||||
Ok(0),
|
||||
Unavailable(1),
|
||||
Expired(2),
|
||||
UnknownCert(3),
|
||||
Revoked(4),
|
||||
InvalidEncoding(5),
|
||||
InvalidExtension(6),
|
||||
}
|
||||
|
||||
// Marked private as this is not meant to be used in Android code.
|
||||
private class VerificationResult(
|
||||
status: StatusCode,
|
||||
@Suppress("unused") val message: String? = null
|
||||
) {
|
||||
@Suppress("unused")
|
||||
private val code: Int = status.value
|
||||
}
|
||||
|
||||
// NOTE: All TrustManager and certificate validation methods are not thread safe. These
|
||||
// are all guarded by Kotlin's `Synchronized` accessors to prevent undefined behavior.
|
||||
|
||||
// Only JNI and test code calls this, so unused code warnings are suppressed.
|
||||
// Internal for test code - no other Kotlin code should use this object directly.
|
||||
@Suppress("unused")
|
||||
// We want to show a difference between Kotlin-side logs and those in Rust code
|
||||
@SuppressLint("LongLogTag")
|
||||
internal object CertificateVerifier {
|
||||
private const val TAG = "rustls-platform-verifier-android"
|
||||
|
||||
private fun createTrustManager(keystore: KeyStore?): X509TrustManagerExtensions? {
|
||||
// This can never throw since the default algorithm is used.
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
|
||||
factory.init(keystore)
|
||||
|
||||
val availableTrustManagers = try {
|
||||
factory.trustManagers
|
||||
} catch (e: RuntimeException) {
|
||||
Log.w(TAG, "exception thrown creating a TrustManager: $e")
|
||||
return null
|
||||
}
|
||||
|
||||
for (manager in availableTrustManagers) {
|
||||
if (manager is X509TrustManager) {
|
||||
// Kotlin ensures this can't throw at runtime since it knows that
|
||||
// it must be the correct type by now.
|
||||
return X509TrustManagerExtensions(manager)
|
||||
}
|
||||
}
|
||||
|
||||
Log.e(TAG, "failed to find a usable trust manager")
|
||||
return null
|
||||
}
|
||||
|
||||
private fun makeLazyTrustManager(keystore: KeyStore?): Lazy<X509TrustManagerExtensions?> {
|
||||
// Ensure the keystore is loaded. Since all of the trust managers are initialized in a
|
||||
// `Lazy`, this will only run once.
|
||||
keystore?.load(null)
|
||||
|
||||
return lazy { createTrustManager(keystore) }
|
||||
}
|
||||
|
||||
// -- Test only --
|
||||
// Ideally, all of this will be optimized out at compile time due to not being accessed
|
||||
// in release builds.
|
||||
|
||||
@get:Synchronized
|
||||
private val mockKeystore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
|
||||
@get:Synchronized
|
||||
private var mockTrustManager: Lazy<X509TrustManagerExtensions?> =
|
||||
makeLazyTrustManager(mockKeystore)
|
||||
|
||||
@JvmStatic
|
||||
private fun addMockRoot(root: ByteArray) {
|
||||
if (!BuildConfig.TEST) {
|
||||
throw Exception("attempted to add a mock root outside a test!")
|
||||
}
|
||||
|
||||
val alias = "root_${mockKeystore.size()}"
|
||||
// Throwing here is fine since test roots should always be well-formed
|
||||
val cert = certFactory.generateCertificate(ByteArrayInputStream(root))
|
||||
mockKeystore.setCertificateEntry(alias, cert)
|
||||
|
||||
reloadMockData()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun clearMockRoots() {
|
||||
// Reload to get a completely fresh internal state
|
||||
mockKeystore.load(null)
|
||||
reloadMockData()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun reloadMockData() {
|
||||
if (mockTrustManager.isInitialized()) {
|
||||
mockTrustManager = makeLazyTrustManager(mockKeystore)
|
||||
}
|
||||
}
|
||||
|
||||
// Get a list of the system's root CAs.
|
||||
// Function is public for testing only.
|
||||
@JvmStatic
|
||||
fun getSystemRootCAs(): List<X509Certificate> {
|
||||
val rootCAs = mutableListOf<X509Certificate>()
|
||||
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(systemKeystore)
|
||||
|
||||
val availableTrustManagers = try {
|
||||
factory.trustManagers
|
||||
} catch (e: RuntimeException) {
|
||||
Log.w(TAG, "exception thrown creating a TrustManager: $e")
|
||||
return rootCAs
|
||||
}
|
||||
|
||||
availableTrustManagers.forEach { trustManager ->
|
||||
if (trustManager is X509TrustManager) {
|
||||
rootCAs.addAll(trustManager.acceptedIssuers)
|
||||
}
|
||||
}
|
||||
|
||||
return rootCAs
|
||||
}
|
||||
|
||||
// -- End testing requirements --
|
||||
|
||||
private val certFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
|
||||
|
||||
private var systemTrustAnchorCache = hashSetOf<Pair<X500Principal, PublicKey>>()
|
||||
|
||||
@get:Synchronized
|
||||
private var systemCertificateDirectory: File? = System.getenv("ANDROID_ROOT")?.let { rootPath ->
|
||||
File("$rootPath/etc/security/cacerts")
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
private val systemKeystore: KeyStore? = try {
|
||||
KeyStore.getInstance("AndroidCAStore")
|
||||
} catch (_: KeyStoreException) {
|
||||
null
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
private val systemTrustManager: Lazy<X509TrustManagerExtensions?> =
|
||||
makeLazyTrustManager(systemKeystore)
|
||||
|
||||
@JvmStatic
|
||||
private fun verifyCertificateChain(
|
||||
@Suppress("UNUSED_PARAMETER") context: Context,
|
||||
serverName: String,
|
||||
authMethod: String,
|
||||
allowedEkus: Array<String>,
|
||||
ocspResponse: ByteArray?,
|
||||
time: Long,
|
||||
certChain: Array<ByteArray>
|
||||
): VerificationResult {
|
||||
// Convert the array of (supposedly) DER bytes into certificates.
|
||||
val certificateChain = mutableListOf<X509Certificate>()
|
||||
certChain.forEach { certBytes ->
|
||||
val certificate = try {
|
||||
certFactory.generateCertificate(ByteArrayInputStream(certBytes))
|
||||
} catch (e: CertificateException) {
|
||||
return VerificationResult(StatusCode.InvalidEncoding)
|
||||
}
|
||||
certificateChain.add(certificate as X509Certificate)
|
||||
}
|
||||
|
||||
// Will never throw `ArrayIndexOutOfBoundsException` because `rustls`'s `ServerCertVerifier` trait
|
||||
// has a mandatory `end_entity` parameter in `verify_server_cert`.
|
||||
val endEntity = certificateChain[0]
|
||||
|
||||
// Check that the certificate is valid at the point of time provided by `rustls`.
|
||||
try {
|
||||
endEntity.checkValidity(Date(time))
|
||||
} catch (e: CertificateExpiredException) {
|
||||
return VerificationResult(StatusCode.Expired)
|
||||
} catch (e: CertificateNotYetValidException) {
|
||||
return VerificationResult(StatusCode.Expired)
|
||||
}
|
||||
|
||||
// Check that this certificate can be used in a TLS server.
|
||||
if (!verifyCertUsage(endEntity, allowedEkus)) {
|
||||
return VerificationResult(StatusCode.InvalidExtension)
|
||||
}
|
||||
|
||||
// Select the trust manager to use.
|
||||
//
|
||||
// We select them as follows:
|
||||
// - If built for release, only use the system trust manager. This should let all test-related
|
||||
// code be optimized out.
|
||||
// - If built for tests:
|
||||
// - If the mock CA store has any values, use the mock trust manager.
|
||||
// - Otherwise, use the system trust manager.
|
||||
val (trustManager, keystore) = if (!BuildConfig.TEST) {
|
||||
val trustManager =
|
||||
systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable)
|
||||
Pair(trustManager, systemKeystore)
|
||||
} else {
|
||||
if (mockKeystore.size() != 0) {
|
||||
val trustManager = mockTrustManager.value!!
|
||||
Pair(trustManager, mockKeystore)
|
||||
} else {
|
||||
val trustManager =
|
||||
systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable)
|
||||
Pair(trustManager, systemKeystore)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the certificate chain is valid and correct, and nothing more.
|
||||
//
|
||||
// NOTE: This does not validate `serverName` is valid for the end-entity certificate.
|
||||
// That is handled in Rust as Android/Java do not currently provide a RFC 6125 compliant
|
||||
// hostname verifier. Additionally, even the RFC 2818 verifier is not available until API 24.
|
||||
//
|
||||
// `serverName` is only used for pinning/CT requirements.
|
||||
//
|
||||
// Returns the "the properly ordered chain used for verification as a list of X509Certificates.",
|
||||
// meaning a list from end-entity certificate to trust-anchor.
|
||||
val validChain = try {
|
||||
trustManager.checkServerTrusted(certificateChain.toTypedArray(), authMethod, serverName)
|
||||
} catch (e: CertificateException) {
|
||||
// In test configurations we may see `checkServerTrusted` fail once vendored test
|
||||
// certificates pass their expiry date. We try to avoid that by using a fixed
|
||||
// verification time when calling `endEntity.checkValidity` above, however we can't
|
||||
// fix the time for the `checkServerTrusted` call.
|
||||
//
|
||||
// To make diagnosing CI test failures easier we try to find the root cause of
|
||||
// checkServerTrusted failing, returning a different `StatusCode` as appropriate.
|
||||
if (BuildConfig.TEST) {
|
||||
var rootCause: Throwable? = e
|
||||
while (rootCause?.cause != null && rootCause.cause != rootCause) {
|
||||
rootCause = rootCause.cause
|
||||
}
|
||||
return when (rootCause) {
|
||||
is CertificateExpiredException, is CertificateNotYetValidException -> VerificationResult(
|
||||
StatusCode.Expired,
|
||||
rootCause.toString()
|
||||
)
|
||||
|
||||
else -> VerificationResult(StatusCode.UnknownCert, rootCause.toString())
|
||||
}
|
||||
}
|
||||
// In non-test configurations we should have caught expiry errors earlier and
|
||||
// can simply return an unknown cert error without digging through the exception
|
||||
// cause chain.
|
||||
return VerificationResult(StatusCode.UnknownCert, e.toString())
|
||||
}
|
||||
|
||||
// TEST ONLY: Mock test suite cannot attempt to check revocation status if no OSCP data has been stapled,
|
||||
// because Android requires certificates to an specify OCSP responder for network fetch in this case.
|
||||
// If in testing w/o OCSP stapled, short-circuit here - only prior checks apply.
|
||||
if (BuildConfig.TEST && (mockKeystore.size() != 0) && (ocspResponse == null)) {
|
||||
return VerificationResult(StatusCode.Ok)
|
||||
}
|
||||
|
||||
// Try to check the revocation status of the cert, if it is supported.
|
||||
//
|
||||
// This is supported at >= API 24, but we're supporting 22 (Android 5) for the best
|
||||
// compatibility.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Note:
|
||||
//
|
||||
// 1. Android does not provide any way only to attempt to validate revocation from cached
|
||||
// data like the other platforms do. This means it will always use the network for
|
||||
// certificates which had no stapled response.
|
||||
//
|
||||
// 2: Likely because of 1, Android requires all issued certificates to have some form of
|
||||
// revocation included in their authority information. This doesn't work universally as
|
||||
// issuing certificates in use may omit authority access information (for example the
|
||||
// Let's Encrypt R3 Intermediate Certificate).
|
||||
//
|
||||
// Given these constraints, the best option is to only check revocation information
|
||||
// at the end-entity depth. We will prefer OCSP (to use stapled information if possible).
|
||||
// If there is no stapled OCSP response, Android may use the network to attempt to fetch
|
||||
// one. If OCSP checking fails, it may fall back to fetching CRLs. We allow "soft"
|
||||
// failures, for example transient network errors.
|
||||
//
|
||||
// In the case of a non-public root, such as an internal CA or self-signed certificate,
|
||||
// we opt to skip revocation checks entirely. The only exception is if the server
|
||||
// provided stapled OCSP data, which is an explicit signal and won't introduce non-ideal
|
||||
// platform behavior when attempting validation.
|
||||
//
|
||||
// This is because these are cases where a user or administrator has explicitly opted to
|
||||
// trust a certificate they (at least believe) have control over. These certificates rarely
|
||||
// contain revocation information as well, so these cases don't lose much.
|
||||
// See https://github.com/rustls/rustls-platform-verifier/issues/69 as well.
|
||||
if (ocspResponse == null && !isKnownRoot(validChain.last())) {
|
||||
// Chain validation must have succeeded by this point.
|
||||
return VerificationResult(StatusCode.Ok)
|
||||
}
|
||||
|
||||
val parameters = PKIXBuilderParameters(keystore, null)
|
||||
|
||||
val validator = CertPathValidator.getInstance("PKIX")
|
||||
val revocationChecker = validator.revocationChecker as PKIXRevocationChecker
|
||||
|
||||
revocationChecker.options = EnumSet.of(
|
||||
PKIXRevocationChecker.Option.SOFT_FAIL,
|
||||
PKIXRevocationChecker.Option.ONLY_END_ENTITY
|
||||
)
|
||||
|
||||
// Use the OCSP data `rustls` provided, if present.
|
||||
// Its expected that the server only sends revocation data for its own leaf certificate.
|
||||
//
|
||||
// If this field is set, then Android will use it and skip any networking to
|
||||
// attempt a fetch for that certificate. Otherwise, it will attempt to fetch it from the network.
|
||||
// Ref: https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/sun/security/provider/certpath/RevocationChecker.java;l=694
|
||||
ocspResponse?.let { providedResponse ->
|
||||
revocationChecker.ocspResponses = mapOf(endEntity to providedResponse)
|
||||
}
|
||||
|
||||
// Use the custom revocation definition.
|
||||
// "Note that when a `PKIXRevocationChecker` is added to `PKIXParameters`, it clones the `PKIXRevocationChecker`;
|
||||
// thus any subsequent modifications to the `PKIXRevocationChecker` have no effect."
|
||||
// - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker
|
||||
parameters.certPathCheckers = listOf(revocationChecker)
|
||||
// "When supplying a revocation checker in this manner, it will be used to check revocation
|
||||
// irrespective of the setting of the `RevocationEnabled` flag."
|
||||
// - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker
|
||||
parameters.isRevocationEnabled = false
|
||||
|
||||
// Validate the revocation status of the end entity certificate.
|
||||
try {
|
||||
validator.validate(certFactory.generateCertPath(validChain), parameters)
|
||||
} catch (e: CertPathValidatorException) {
|
||||
// LetsEncrypt no longer include OCSP information (as OCSP is being deprecated) which Android is not
|
||||
// happy with since it *only* tries OCSP by default. We aren't 100% decided on how to fix this yet for real
|
||||
// (see https://github.com/rustls/rustls-platform-verifier/pull/179) so for now we implement an out for
|
||||
// tests to allow regular maintenance to proceed.
|
||||
if (BuildConfig.TEST && e.reason == CertPathValidatorException.BasicReason.UNSPECIFIED) {
|
||||
return VerificationResult(StatusCode.Ok)
|
||||
}
|
||||
|
||||
return VerificationResult(StatusCode.Revoked, e.toString())
|
||||
}
|
||||
} else {
|
||||
// This is allowed to be skipped since revocation checking is best-effort.
|
||||
Log.w(TAG, "did not attempt to validate OCSP due to Android version")
|
||||
}
|
||||
|
||||
return VerificationResult(StatusCode.Ok)
|
||||
}
|
||||
|
||||
private fun verifyCertUsage(certificate: X509Certificate, allowedEkus: Array<String>): Boolean {
|
||||
val ekus = try {
|
||||
certificate.extendedKeyUsage
|
||||
}
|
||||
// This should be unreachable, but could happen.
|
||||
catch (_: CertificateParsingException) {
|
||||
return false
|
||||
} catch (_: NullPointerException) {
|
||||
// According to Chromium's implementation, this can crash when the EKU data is malformed.
|
||||
Log.w(TAG, "exception handling certificate EKU")
|
||||
return false
|
||||
} ?: return true // If the list is empty, we have nothing to do.
|
||||
|
||||
return ekus.any { allowedEkus.contains(it) }
|
||||
}
|
||||
|
||||
// Android hashes a principal using the first four bytes of its MD5 digest, encoded in
|
||||
// lowercase hex and reversed.
|
||||
//
|
||||
// Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=339
|
||||
private fun hashPrincipal(principal: X500Principal): String {
|
||||
val hexDigits = "0123456789abcdef".toCharArray()
|
||||
val digest = MessageDigest.getInstance("MD5").digest(principal.encoded)
|
||||
val hexChars = CharArray(8)
|
||||
|
||||
for (i in 0..3) {
|
||||
// Kotlin doesn't support bitwise operators for bytes, only Int and Long.
|
||||
val digestByte = digest[3 - i].toInt()
|
||||
hexChars[2 * i] = hexDigits[(digestByte shr 4) and 0xf]
|
||||
hexChars[2 * i + 1] = hexDigits[digestByte and 0xf]
|
||||
}
|
||||
|
||||
return String(hexChars)
|
||||
}
|
||||
|
||||
// Check if CA root is known or not.
|
||||
// Known means installed in root CA store, either a preset public CA or a custom one installed by an enterprise/user.
|
||||
//
|
||||
// Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=351
|
||||
fun isKnownRoot(root: X509Certificate): Boolean {
|
||||
// System keystore and cert directory must be non-null to perform checking
|
||||
systemKeystore?.let { loadedSystemKeystore ->
|
||||
systemCertificateDirectory?.let { loadedSystemCertificateDirectory ->
|
||||
|
||||
// Check the in-memory cache first
|
||||
val key = Pair(root.subjectX500Principal, root.publicKey)
|
||||
if (systemTrustAnchorCache.contains(key)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// System trust anchors are stored under a hash of the principal.
|
||||
// In case of collisions, append number.
|
||||
val hash = hashPrincipal(root.subjectX500Principal)
|
||||
var i = 0
|
||||
while (true) {
|
||||
val alias = "$hash.$i"
|
||||
|
||||
if (!File(loadedSystemCertificateDirectory, alias).exists()) {
|
||||
break
|
||||
}
|
||||
|
||||
val anchor = loadedSystemKeystore.getCertificate("system:$alias")
|
||||
|
||||
// It's possible for `anchor` to be `null` if the user deleted a trust anchor.
|
||||
// Continue iterating as there may be further collisions after the deleted anchor.
|
||||
if (anchor == null) {
|
||||
continue
|
||||
// This should never happen
|
||||
} else if (anchor !is X509Certificate) {
|
||||
// SAFETY: This logs a unique identifier (hash value) only in cases where a file within the
|
||||
// system's root trust store is not a valid X509 certificate (extremely unlikely error).
|
||||
// The hash doesn't tell us any sensitive information about the invalid cert or reveal any of
|
||||
// its contents - it just lets us ID the bad file if a user is having TLS failure issues.
|
||||
Log.e(TAG, "anchor is not a certificate, alias: $alias")
|
||||
continue
|
||||
// If subject and public key match, it's a system root.
|
||||
} else {
|
||||
if ((root.subjectX500Principal == anchor.subjectX500Principal) && (root.publicKey == anchor.publicKey)) {
|
||||
systemTrustAnchorCache.add(key)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found in cache or store: non-public
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -400,6 +400,7 @@ fun TextComposer(
|
|||
onAddAttachment = onAddAttachment,
|
||||
onDeleteVoiceMessage = onDeleteVoiceMessage,
|
||||
onVoiceRecorderEvent = onVoiceRecorderEvent,
|
||||
onResetComposerMode = onResetComposerMode,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -409,6 +410,15 @@ fun TextComposer(
|
|||
|
||||
SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it }
|
||||
|
||||
// Re-focus the text input when voice recording ends so the user can continue typing
|
||||
var previousVoiceMessageState by remember { mutableStateOf(voiceMessageState) }
|
||||
LaunchedEffect(voiceMessageState) {
|
||||
if (voiceMessageState is VoiceMessageState.Idle && previousVoiceMessageState !is VoiceMessageState.Idle) {
|
||||
onRequestFocus()
|
||||
}
|
||||
previousVoiceMessageState = voiceMessageState
|
||||
}
|
||||
|
||||
val latestOnReceiveSuggestion by rememberUpdatedState(onReceiveSuggestion)
|
||||
if (state is TextEditorState.Rich) {
|
||||
val menuAction = state.richTextEditorState.menuAction
|
||||
|
|
@ -440,6 +450,7 @@ private fun StandardLayout(
|
|||
onAddAttachment: () -> Unit,
|
||||
onDeleteVoiceMessage: () -> Unit,
|
||||
onVoiceRecorderEvent: (VoiceMessageRecorderEvent) -> Unit,
|
||||
onResetComposerMode: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
|
|
@ -506,6 +517,14 @@ private fun StandardLayout(
|
|||
) {
|
||||
if (voiceMessageState is VoiceMessageState.Idle) {
|
||||
textInput()
|
||||
} else if (composerMode is MessageComposerMode.Special) {
|
||||
TextInputBox(
|
||||
composerMode = composerMode,
|
||||
onResetComposerMode = onResetComposerMode,
|
||||
isTextEmpty = true,
|
||||
) {
|
||||
voiceRecording()
|
||||
}
|
||||
} else {
|
||||
voiceRecording()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,18 @@
|
|||
<string name="rich_text_editor_bullet_list">"Chuyển đổi danh sách dấu đầu dòng"</string>
|
||||
<string name="rich_text_editor_close_formatting_options">"Hủy và đóng định dạng văn bản"</string>
|
||||
<string name="rich_text_editor_code_block">"Bật/tắt khối mã"</string>
|
||||
<string name="rich_text_editor_composer_caption_placeholder">"Thêm chú thích"</string>
|
||||
<string name="rich_text_editor_composer_encrypted_placeholder">"Tin nhắn được mã hóa…"</string>
|
||||
<string name="rich_text_editor_composer_placeholder">"Tin nhắn…"</string>
|
||||
<string name="rich_text_editor_composer_unencrypted_placeholder">"Tin nhắn chưa được mã hóa…"</string>
|
||||
<string name="rich_text_editor_create_link">"Tạo liên kết"</string>
|
||||
<string name="rich_text_editor_edit_link">"Sửa liên kết"</string>
|
||||
<string name="rich_text_editor_format_action">"%1$s, tình trạng: %2$s"</string>
|
||||
<string name="rich_text_editor_format_bold">"Áp dụng định dạng in đậm"</string>
|
||||
<string name="rich_text_editor_format_italic">"Áp dụng định dạng in nghiêng"</string>
|
||||
<string name="rich_text_editor_format_state_disabled">"Đã tắt"</string>
|
||||
<string name="rich_text_editor_format_state_off">"tắt"</string>
|
||||
<string name="rich_text_editor_format_state_on">"bật"</string>
|
||||
<string name="rich_text_editor_format_strikethrough">"Áp dụng định dạng gạch ngang"</string>
|
||||
<string name="rich_text_editor_format_underline">"Áp dụng định dạng gạch chân"</string>
|
||||
<string name="rich_text_editor_full_screen_toggle">"Bật/tắt chế độ toàn màn hình"</string>
|
||||
|
|
|
|||
|
|
@ -474,7 +474,6 @@ Opravdu chcete pokračovat?"</string>
|
|||
<string name="screen_create_poll_remove_accessibility_label">"Odstranit %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Nastavení"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Výběr média se nezdařil, zkuste to prosím znovu."</string>
|
||||
<string name="screen_onboarding_welcome_back">"Vítejte zpět"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Přidržte zprávu a vyberte „%1$s“, kterou chcete zahrnout sem."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Připněte důležité zprávy, aby je bylo možné snadno najít"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
|
|
@ -466,7 +466,6 @@ Er du sikker på, at du vil fortsætte?"</string>
|
|||
<string name="screen_create_poll_remove_accessibility_label">"Fjern %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Indstillinger"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Det lykkedes ikke at vælge medie. Prøv igen."</string>
|
||||
<string name="screen_onboarding_welcome_back">"Velkommen tilbage"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Tryk på en besked og vælg \"%1$s\" for at inkludere den her."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Fastgør vigtige beskeder, så de let kan opdages"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_add_reaction">"Reaktion hinzufügen: %1$s"</string>
|
||||
<string name="a11y_address">"Adresse"</string>
|
||||
<string name="a11y_avatar">"Avatar"</string>
|
||||
<string name="a11y_collapse_message_text_field">"Nachrichtentextfeld minimieren"</string>
|
||||
<string name="a11y_delete">"Löschen"</string>
|
||||
|
|
@ -26,9 +27,12 @@
|
|||
<string name="a11y_pause">"Pausieren"</string>
|
||||
<string name="a11y_paused_voice_message">"Sprachnachricht, Dauer:%1$s, aktuelle Position: %2$s"</string>
|
||||
<string name="a11y_pin_field">"PIN-Feld"</string>
|
||||
<string name="a11y_pinned_location">"Fixierter Standort"</string>
|
||||
<string name="a11y_play">"Abspielen"</string>
|
||||
<string name="a11y_playback_speed">"Wiedergabegeschwindigkeit"</string>
|
||||
<string name="a11y_poll">"Umfrage"</string>
|
||||
<string name="a11y_poll_end">"Umfrage beendet"</string>
|
||||
<string name="a11y_qr_code">"QR-Code"</string>
|
||||
<string name="a11y_react_with">"Reagiere mit %1$s"</string>
|
||||
<string name="a11y_react_with_other_emojis">"Mit anderen Emojis reagieren"</string>
|
||||
<string name="a11y_read_receipts_multiple">"Gelesen von %1$s und %2$s"</string>
|
||||
|
|
@ -42,9 +46,11 @@
|
|||
<string name="a11y_remove_reaction_with">"Entferne Reaktionen mit %1$s"</string>
|
||||
<string name="a11y_room_avatar">"Avatar"</string>
|
||||
<string name="a11y_send_files">"Dateien senden"</string>
|
||||
<string name="a11y_sender_location">"Standort des Absenders"</string>
|
||||
<string name="a11y_session_verification_time_limited_action_required">"Zeitlich begrenzte Handlung erforderlich, du hast eine Minute Zeit zur Verifizierung"</string>
|
||||
<string name="a11y_show_password">"Passwort anzeigen"</string>
|
||||
<string name="a11y_start_call">"Anruf starten"</string>
|
||||
<string name="a11y_start_voice_call">"Sprachanruf starten"</string>
|
||||
<string name="a11y_tombstoned_room">"Stillgelegter Chat"</string>
|
||||
<string name="a11y_user_avatar">"Nutzer-Avatar"</string>
|
||||
<string name="a11y_user_menu">"Nutzer-Menü"</string>
|
||||
|
|
@ -114,6 +120,7 @@
|
|||
<string name="action_leave_space">"Space verlassen"</string>
|
||||
<string name="action_load_more">"Mehr laden…"</string>
|
||||
<string name="action_manage_account">"Konto verwalten"</string>
|
||||
<string name="action_manage_account_and_devices">"Konto & Geräte verwalten"</string>
|
||||
<string name="action_manage_devices">"Geräte verwalten"</string>
|
||||
<string name="action_manage_rooms">"Chats und Gruppen konfigurieren"</string>
|
||||
<string name="action_message">"Nachricht"</string>
|
||||
|
|
@ -153,16 +160,18 @@
|
|||
<string name="action_send_voice_message">"Sprachnachricht senden"</string>
|
||||
<string name="action_share">"Teilen"</string>
|
||||
<string name="action_share_link">"Link teilen"</string>
|
||||
<string name="action_share_live_location">"Live-Standort teilen"</string>
|
||||
<string name="action_show">"Zeige"</string>
|
||||
<string name="action_sign_in_again">"Erneut anmelden"</string>
|
||||
<string name="action_signout">"Abmelden"</string>
|
||||
<string name="action_signout_anyway">"Trotzdem abmelden"</string>
|
||||
<string name="action_signout">"Dieses Gerät entfernen"</string>
|
||||
<string name="action_signout_anyway">"Dieses Gerät trotzdem entfernen"</string>
|
||||
<string name="action_skip">"Überspringen"</string>
|
||||
<string name="action_start">"Start"</string>
|
||||
<string name="action_start_chat">"Chat starten"</string>
|
||||
<string name="action_start_over">"Neu beginnen"</string>
|
||||
<string name="action_start_verification">"Verifizierung starten"</string>
|
||||
<string name="action_static_map_load">"Tippe, um die Karte zu laden"</string>
|
||||
<string name="action_stop">"Beenden"</string>
|
||||
<string name="action_take_photo">"Foto aufnehmen"</string>
|
||||
<string name="action_tap_for_options">"Für Optionen tippen"</string>
|
||||
<string name="action_translate">"Übersetzen"</string>
|
||||
|
|
@ -183,6 +192,7 @@
|
|||
<string name="common_advanced_settings">"Erweiterte Einstellungen"</string>
|
||||
<string name="common_an_image">"ein Bild"</string>
|
||||
<string name="common_analytics">"Analysedaten"</string>
|
||||
<string name="common_android_fetching_notifications_title">"Benachrichtigungen werden synchronisiert…"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_left_room">"Du hast den Chat verlassen"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_session_logged_out">"Du wurdest aus der Sitzung abgemeldet."</string>
|
||||
<string name="common_appearance">"Erscheinungsbild"</string>
|
||||
|
|
@ -216,6 +226,7 @@
|
|||
<string name="common_empty_file">"Leere Datei"</string>
|
||||
<string name="common_encryption">"Verschlüsselung"</string>
|
||||
<string name="common_encryption_enabled">"Verschlüsselung aktiviert"</string>
|
||||
<string name="common_ends_at">"Endet um %1$s"</string>
|
||||
<string name="common_enter_your_pin">"PIN eingeben"</string>
|
||||
<string name="common_error">"Fehler"</string>
|
||||
<string name="common_error_registering_pusher_android">"Es ist ein Fehler aufgetreten. Du erhältst eventuell keine Benachrichtigungen für neue Nachrichten. Bitte behebe den Fehler in den Einstellungen.
|
||||
|
|
@ -242,6 +253,8 @@ Grund: %1$s."</string>
|
|||
<string name="common_line_copied_to_clipboard">"Zeile in die Zwischenablage kopiert"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Link in die Zwischenablage kopiert"</string>
|
||||
<string name="common_link_new_device">"Neues Gerät verknüpfen"</string>
|
||||
<string name="common_live_location">"Live-Standort"</string>
|
||||
<string name="common_live_location_ended">"Live-Standort teilen beendet"</string>
|
||||
<string name="common_loading">"Laden…"</string>
|
||||
<string name="common_loading_more">"Mehr wird geladen…"</string>
|
||||
<plurals name="common_many_members">
|
||||
|
|
@ -268,6 +281,7 @@ Grund: %1$s."</string>
|
|||
<string name="common_offline">"Offline"</string>
|
||||
<string name="common_open_source_licenses">"Open-Source-Lizenzen"</string>
|
||||
<string name="common_or">"oder"</string>
|
||||
<string name="common_other_options">"Weitere Optionen"</string>
|
||||
<string name="common_password">"Passwort"</string>
|
||||
<string name="common_people">"Personen"</string>
|
||||
<string name="common_permalink">"Permalink"</string>
|
||||
|
|
@ -285,8 +299,10 @@ Grund: %1$s."</string>
|
|||
</plurals>
|
||||
<string name="common_preparing">"Vorbereitung läuft …"</string>
|
||||
<string name="common_privacy_policy">"Datenschutzerklärung"</string>
|
||||
<string name="common_private">"Privat"</string>
|
||||
<string name="common_private_room">"Privater Chat"</string>
|
||||
<string name="common_private_space">"Privater Space"</string>
|
||||
<string name="common_public">"Öffentlich"</string>
|
||||
<string name="common_public_room">"Öffentlicher Chat"</string>
|
||||
<string name="common_public_space">"Öffentlicher Space"</string>
|
||||
<string name="common_reaction">"Reaktion"</string>
|
||||
|
|
@ -335,12 +351,14 @@ Grund: %1$s."</string>
|
|||
<string name="common_settings">"Einstellungen"</string>
|
||||
<string name="common_share_space">"Space teilen"</string>
|
||||
<string name="common_shared_history">"Neue Mitglieder sehen den Nachrichtenverlauf"</string>
|
||||
<string name="common_shared_live_location">"Geteilter Live-Standort"</string>
|
||||
<string name="common_shared_location">"Geteilter Standort"</string>
|
||||
<string name="common_shared_space">"Gemeinsamer Space"</string>
|
||||
<string name="common_signing_out">"Abmelden"</string>
|
||||
<string name="common_signing_out">"Gerät entfernen"</string>
|
||||
<string name="common_something_went_wrong">"Es ist ein Fehler aufgetreten."</string>
|
||||
<string name="common_something_went_wrong_message">"Wir haben ein Problem festgestellt. Bitte versuch es erneut."</string>
|
||||
<string name="common_space">"Space"</string>
|
||||
<string name="common_space_members">"Space Mitglieder"</string>
|
||||
<string name="common_space_topic_placeholder">"Worum geht es hier?"</string>
|
||||
<plurals name="common_spaces">
|
||||
<item quantity="one">"%1$d Space"</item>
|
||||
|
|
@ -356,12 +374,13 @@ Grund: %1$s."</string>
|
|||
<string name="common_text">"Text"</string>
|
||||
<string name="common_third_party_notices">"Hinweise von Drittanbietern"</string>
|
||||
<string name="common_thread">"Thread"</string>
|
||||
<string name="common_threads">"Threads"</string>
|
||||
<string name="common_topic">"Thema"</string>
|
||||
<string name="common_topic_placeholder">"Worum geht is in diesem Chat?"</string>
|
||||
<string name="common_unable_to_decrypt">"Entschlüsselung nicht möglich"</string>
|
||||
<string name="common_unable_to_decrypt_insecure_device">"Von einem ungesicherten Gerät gesendet"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"Du hast keinen Zugriff auf diese Nachricht."</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"Die verifizierte Identität des Senders hat sich geändert"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"Die verifizierte Identität des Senders wurde zurückgesetzt"</string>
|
||||
<string name="common_unable_to_invite_message">"Einladungen konnten nicht an einen oder mehrere Nutzer gesendet werden."</string>
|
||||
<string name="common_unable_to_invite_title">"Einladung(en) konnte(n) nicht gesendet werden"</string>
|
||||
<string name="common_unlock">"Entsperren"</string>
|
||||
|
|
@ -386,17 +405,19 @@ Grund: %1$s."</string>
|
|||
<string name="common_voice_message">"Sprachnachricht"</string>
|
||||
<string name="common_waiting">"Warten…"</string>
|
||||
<string name="common_waiting_for_decryption_key">"Warte auf diese Nachricht"</string>
|
||||
<string name="common_waiting_live_location">"Warten auf Live-Standort…"</string>
|
||||
<string name="common_world_readable_history">"Jeder kann den Nachrichtenverlauf sehen"</string>
|
||||
<string name="common_you">"Du"</string>
|
||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) hat diese Nachricht geteilt, weil du nicht im Chat warst, als sie verschickt wurde."</string>
|
||||
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"Diese Nachricht wurde von %1$s weitergeleitet, da du zum Zeitpunkt des Versands kein Mitglied der Gruppe warst."</string>
|
||||
<string name="crypto_history_visible">"Diese Gruppe wurde so konfiguriert, dass neue Mitglieder den vergangenen Nachrichtenverlauf lesen können. %1$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"%1$s\'s Identität has sich geändert. %2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"%1$s\'s %2$s Identität hat sich geändert. %3$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"Die Identität von %1$s wurde zurückgesetzt. %2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"Die Identität von %1$s %2$s wurde zurückgesetzt. %3$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
|
||||
<string name="crypto_identity_change_profile_pin_violation">"Die Identität von %1$s hat sich geändert."</string>
|
||||
<string name="crypto_identity_change_verification_violation_new">"Die Identität von %1$s\'s %2$s hat sich geändert. %3$s"</string>
|
||||
<string name="crypto_identity_change_profile_pin_violation">"Die Identität von %1$s wurde zurückgesetzt."</string>
|
||||
<string name="crypto_identity_change_verification_violation_new">"Die Identität von %1$s %2$s wurde zurückgesetzt. %3$s"</string>
|
||||
<string name="crypto_identity_change_withdraw_verification_action">"Verifizierung zurückziehen"</string>
|
||||
<string name="dialog_allow_access">"Zugriff erlauben"</string>
|
||||
<string name="dialog_confirm_link_message">"Der Link %1$s führt dich zu einer anderen Seite %2$s.
|
||||
|
||||
Möchtest du wirklich fortfahren?"</string>
|
||||
|
|
@ -453,7 +474,7 @@ Möchtest du wirklich fortfahren?"</string>
|
|||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Fixierte Nachrichten"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"Du wirst jetzt zu deinem %1$s Konto geleitet, um deine Identität zurückzusetzen. Danach wirst du zur App zurückgebracht."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Kannst du das nicht bestätigen? Gehe zu deinem Konto, um deine Identität zurückzusetzen."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Bestätigung nicht möglich? Rufe dein Konto auf, um deine digitale Identität zurückzusetzen."</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"Verifizierung zurückziehen und senden"</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_subtitle">"Du kannst deine Verifizierung zurückziehen und diese Nachricht trotzdem senden, oder du kannst vorerst abbrechen und es später noch einmal versuchen, nachdem du %1$s erneut verifiziert hast."</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_title">"Deine Nachricht wurde nicht gesendet, da die verifizierte Identität von %1$s zurückgesetzt wurde"</string>
|
||||
|
|
@ -480,11 +501,14 @@ Möchtest du wirklich fortfahren?"</string>
|
|||
<string name="screen_share_open_google_maps">"In Google Maps öffnen"</string>
|
||||
<string name="screen_share_open_osm_maps">"In OpenStreetMap öffnen"</string>
|
||||
<string name="screen_share_this_location_action">"Diesen Standort teilen"</string>
|
||||
<string name="screen_sharing_location_option_sheet_title">"Optionen zum Teilen"</string>
|
||||
<string name="screen_space_list_description">"Von dir erstellte oder beigetretene Spaces."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_empty_state_title">"Erstelle einen Space, um Chats zu organisieren"</string>
|
||||
<string name="screen_space_list_parent_space">"%1$s Space"</string>
|
||||
<string name="screen_space_list_title">"Spaces"</string>
|
||||
<string name="screen_static_location_sheet_timestamp_description">"Geteilt %1$s"</string>
|
||||
<string name="screen_static_location_sheet_title">"Auf der Karte"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Die Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Die Nachricht wurde nicht gesendet, weil du eines oder mehrere deiner Geräte nicht verifiziert hast."</string>
|
||||
|
|
|
|||
|
|
@ -459,7 +459,6 @@
|
|||
<string name="screen_create_poll_remove_accessibility_label">"Αφαίρεση %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Ρυθμίσεις"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά."</string>
|
||||
<string name="screen_onboarding_welcome_back">"Καλώς ήρθατε ξανά"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Πάτα σε ένα μήνυμα και επέλεξε «%1$s» για να συμπεριληφθεί εδώ."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Καρφίτσωσε σημαντικά μηνύματα, ώστε να μπορούν να εντοπιστούν εύκολα"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
<string name="a11y_pause">"Peata"</string>
|
||||
<string name="a11y_paused_voice_message">"Häälsõnum, kestus:%1$s, praegune asukoht: %2$s"</string>
|
||||
<string name="a11y_pin_field">"PIN-koodi väli"</string>
|
||||
<string name="a11y_pinned_location">"Esiletõstetud asukoht"</string>
|
||||
<string name="a11y_play">"Esita"</string>
|
||||
<string name="a11y_playback_speed">"Taasesituse kiirus"</string>
|
||||
<string name="a11y_poll">"Küsitlus"</string>
|
||||
|
|
@ -45,9 +46,12 @@
|
|||
<string name="a11y_remove_reaction_with">"Eemalda reageerimine: %1$s"</string>
|
||||
<string name="a11y_room_avatar">"Jututoa tunnuspilt"</string>
|
||||
<string name="a11y_send_files">"Saada faile"</string>
|
||||
<string name="a11y_sender_location">"Saatja asukoht"</string>
|
||||
<string name="a11y_session_verification_time_limited_action_required">"Palun tee see ajapiiranguga toiming, sul on aega üks minut"</string>
|
||||
<string name="a11y_show_password">"Näita salasõna"</string>
|
||||
<string name="a11y_start_call">"Helista"</string>
|
||||
<string name="a11y_start_video_call">"Alusta videokõnet"</string>
|
||||
<string name="a11y_start_voice_call">"Helista"</string>
|
||||
<string name="a11y_tombstoned_room">"Lõpetatuks märgitud jututuba"</string>
|
||||
<string name="a11y_user_avatar">"Kasutaja tunnuspilt"</string>
|
||||
<string name="a11y_user_menu">"Kasutajamenüü"</string>
|
||||
|
|
@ -117,6 +121,7 @@
|
|||
<string name="action_leave_space">"Lahku kogukonnast"</string>
|
||||
<string name="action_load_more">"Näita veel"</string>
|
||||
<string name="action_manage_account">"Halda kasutajakontot"</string>
|
||||
<string name="action_manage_account_and_devices">"Halda kasutajakontosid ja seadmeid"</string>
|
||||
<string name="action_manage_devices">"Halda seadmeid"</string>
|
||||
<string name="action_manage_rooms">"Halda jututuba"</string>
|
||||
<string name="action_message">"Saada sõnum"</string>
|
||||
|
|
@ -156,16 +161,18 @@
|
|||
<string name="action_send_voice_message">"Saada häälsõnum"</string>
|
||||
<string name="action_share">"Jaga"</string>
|
||||
<string name="action_share_link">"Jaga linki"</string>
|
||||
<string name="action_share_live_location">"Jaga asukohta reaalajas"</string>
|
||||
<string name="action_show">"Näita"</string>
|
||||
<string name="action_sign_in_again">"Logi uuesti sisse"</string>
|
||||
<string name="action_signout">"Logi välja"</string>
|
||||
<string name="action_signout_anyway">"Ikkagi logi välja"</string>
|
||||
<string name="action_signout">"Eemalda see seade"</string>
|
||||
<string name="action_signout_anyway">"Eemalda see seade ikkagi"</string>
|
||||
<string name="action_skip">"Jäta vahele"</string>
|
||||
<string name="action_start">"Alusta"</string>
|
||||
<string name="action_start_chat">"Alusta vestlust"</string>
|
||||
<string name="action_start_over">"Alusta uuesti"</string>
|
||||
<string name="action_start_verification">"Alusta verifitseerimist"</string>
|
||||
<string name="action_static_map_load">"Kaardi laadimiseks klõpsa"</string>
|
||||
<string name="action_stop">"Lõpeta"</string>
|
||||
<string name="action_take_photo">"Pildista"</string>
|
||||
<string name="action_tap_for_options">"Valikuteks klõpsa"</string>
|
||||
<string name="action_translate">"Tõlgi"</string>
|
||||
|
|
@ -186,6 +193,7 @@
|
|||
<string name="common_advanced_settings">"Täiendavad seadistused"</string>
|
||||
<string name="common_an_image">"pilt"</string>
|
||||
<string name="common_analytics">"Analüütika"</string>
|
||||
<string name="common_android_fetching_notifications_title">"Sünkroonin teavitusi…"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_left_room">"Sina lahkusid jututoast"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_session_logged_out">"Sa olid sessioonist väljaloginud"</string>
|
||||
<string name="common_appearance">"Välimus"</string>
|
||||
|
|
@ -362,6 +370,7 @@ Põhjus: %1$s."</string>
|
|||
<string name="common_text">"Tekst"</string>
|
||||
<string name="common_third_party_notices">"Kolmandate osapoolte teatised"</string>
|
||||
<string name="common_thread">"Jutulõng"</string>
|
||||
<string name="common_threads">"Jutulõngad"</string>
|
||||
<string name="common_topic">"Teema"</string>
|
||||
<string name="common_topic_placeholder">"Mis on selle jututoa mõte?"</string>
|
||||
<string name="common_unable_to_decrypt">"Dekrüptimine ei olnud võimalik"</string>
|
||||
|
|
@ -450,6 +459,8 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
|||
<string name="screen_create_poll_options_section_title">"Valikud"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Kustuta: %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Seadistused"</string>
|
||||
<string name="screen_live_location_sheet_nobody_sharing">"Mitte keegi ei jaga oma asukohta"</string>
|
||||
<string name="screen_live_location_sheet_sharing_live_location">"Asukoht on jagamisel reaalajas"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Siia lisamiseks vajuta sõnumil ja vali „%1$s“."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Et olulisi sõnumeid oleks lihtsam leida, tõsta nad esile"</string>
|
||||
|
|
@ -486,6 +497,7 @@ Kas sa oled kindel, et soovid jätkata?"</string>
|
|||
<string name="screen_share_open_google_maps">"Ava Google Mapsis"</string>
|
||||
<string name="screen_share_open_osm_maps">"Ava OpenStreetMapis"</string>
|
||||
<string name="screen_share_this_location_action">"Jaga seda asukohta"</string>
|
||||
<string name="screen_sharing_location_option_sheet_title">"Jagamise valikud"</string>
|
||||
<string name="screen_space_list_description">"Sinu loodud kogukonnad ning need, millega oled liitunud."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_empty_state_title">"Jututubade haldamiseks võid luua kogukondi"</string>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
<string name="a11y_session_verification_time_limited_action_required">"Aikarajoitettu toimenpide vaaditaan, sinulla on yksi minuutti aikaa vahvistaa"</string>
|
||||
<string name="a11y_show_password">"Näytä salasana"</string>
|
||||
<string name="a11y_start_call">"Aloita puhelu"</string>
|
||||
<string name="a11y_start_video_call">"Aloita videopuhelu"</string>
|
||||
<string name="a11y_start_voice_call">"Aloita äänipuhelu"</string>
|
||||
<string name="a11y_tombstoned_room">"Haudattu huone"</string>
|
||||
<string name="a11y_user_avatar">"Käyttäjän avatar"</string>
|
||||
|
|
@ -466,14 +467,14 @@ Haluatko varmasti jatkaa?"</string>
|
|||
<string name="screen_create_poll_options_section_title">"Vaihtoehdot"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Poista %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Asetukset"</string>
|
||||
<string name="screen_live_location_sheet_nobody_sharing">"Kukaan ei jaa sijaintiaan"</string>
|
||||
<string name="screen_live_location_sheet_sharing_live_location">"Jaetaan reaaliaikaista sijaintia"</string>
|
||||
<plurals name="screen_live_location_sheet_subtitle">
|
||||
<item quantity="one">"%1$d henkilö"</item>
|
||||
<item quantity="other">"%1$d henkilöä"</item>
|
||||
</plurals>
|
||||
<string name="screen_live_location_sheet_title">"Kartalla"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Median valinta epäonnistui, yritä uudelleen."</string>
|
||||
<string name="screen_missing_key_backup_open_element_classic">"Avaa Element Classic"</string>
|
||||
<string name="screen_missing_key_backup_step_1">"Avaa Element Classic laitteellasi"</string>
|
||||
<string name="screen_missing_key_backup_step_2_android">"Mene kohtaan \"Asetukset\" > \"Tietoturva ja yksityisyys\""</string>
|
||||
<string name="screen_missing_key_backup_step_3_android">"Osiossa \"Salausavainten hallinta\", paina \"Salattujen viestien palautus\"."</string>
|
||||
<string name="screen_missing_key_backup_step_4">"Noudata ohjeita"</string>
|
||||
<string name="screen_missing_key_backup_step_5">"Palaa takaisin %1$s -sovellukseen"</string>
|
||||
<string name="screen_onboarding_welcome_back">"Tervetuloa takaisin"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Paina viestiä ja valitse “%1$s” lisätäksesi sen tänne."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Kiinnitä tärkeät viestit, jotta ne löytyvät helposti."</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
@ -497,6 +498,7 @@ Haluatko varmasti jatkaa?"</string>
|
|||
<string name="screen_room_event_pill">"Viesti huoneessa %1$s"</string>
|
||||
<string name="screen_room_grouped_state_events_expand">"Laajenna"</string>
|
||||
<string name="screen_room_grouped_state_events_reduce">"Pienennä"</string>
|
||||
<string name="screen_room_live_location_banner">"Jaetaan reaaliaikaista sijaintia"</string>
|
||||
<string name="screen_room_permalink_same_room_android">"Katselet jo tätä huonetta!"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s / %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"Kiinnitetty viesti %1$s"</string>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
<string name="a11y_session_verification_time_limited_action_required">"Action limitée dans le temps requise, vous avez une minute pour effectuer la vérification"</string>
|
||||
<string name="a11y_show_password">"Afficher le mot de passe"</string>
|
||||
<string name="a11y_start_call">"Démarrer un appel"</string>
|
||||
<string name="a11y_start_video_call">"Passer un appel vidéo"</string>
|
||||
<string name="a11y_start_voice_call">"Lancer un appel vocal"</string>
|
||||
<string name="a11y_tombstoned_room">"Salon clôturé"</string>
|
||||
<string name="a11y_user_avatar">"Avatar de l’utilisateur"</string>
|
||||
|
|
@ -466,15 +467,14 @@ Raison : %1$s."</string>
|
|||
<string name="screen_create_poll_options_section_title">"Options"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Supprimer %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Paramètres"</string>
|
||||
<string name="screen_live_location_sheet_nobody_sharing">"Personne ne partage sa position"</string>
|
||||
<string name="screen_live_location_sheet_sharing_live_location">"Partage de la position en direct"</string>
|
||||
<plurals name="screen_live_location_sheet_subtitle">
|
||||
<item quantity="one">"%1$d personne"</item>
|
||||
<item quantity="other">"%1$d personnes"</item>
|
||||
</plurals>
|
||||
<string name="screen_live_location_sheet_title">"Sur la carte"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Échec de la sélection du média, veuillez réessayer."</string>
|
||||
<string name="screen_missing_key_backup_open_element_classic">"Ouvrir Element Classic"</string>
|
||||
<string name="screen_missing_key_backup_step_1">"Ouvrez Element Classic sur votre appareil"</string>
|
||||
<string name="screen_missing_key_backup_step_2_android">"Aller à Paramètres > Sécurité et vie privée"</string>
|
||||
<string name="screen_missing_key_backup_step_3_android">"Dans Gestion des clés cryptographiques, sélectionnez Récupération des messages chiffrés"</string>
|
||||
<string name="screen_missing_key_backup_step_4">"Suivez les instructions pour activer votre stockage de clés"</string>
|
||||
<string name="screen_missing_key_backup_step_5">"Revenez à %1$s"</string>
|
||||
<string name="screen_missing_key_backup_title">"Activez le stockage de vos clés avant de continuer avec %1$s"</string>
|
||||
<string name="screen_onboarding_welcome_back">"Bon retour parmi nous"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Cliquez (clic long) sur un message et choisissez « %1$s » pour qu‘il apparaisse ici."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Épinglez les messages importants pour leur donner plus de visibilité"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
@ -498,6 +498,7 @@ Raison : %1$s."</string>
|
|||
<string name="screen_room_event_pill">"Message dans %1$s"</string>
|
||||
<string name="screen_room_grouped_state_events_expand">"Développer"</string>
|
||||
<string name="screen_room_grouped_state_events_reduce">"Réduire"</string>
|
||||
<string name="screen_room_live_location_banner">"Partage de la position en direct"</string>
|
||||
<string name="screen_room_permalink_same_room_android">"Vous êtes déjà dans ce salon!"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s sur %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Messages épinglés"</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_add_reaction">"Dodaj reakciju: %1$s"</string>
|
||||
<string name="a11y_address">"Adresa"</string>
|
||||
<string name="a11y_avatar">"Avatar"</string>
|
||||
<string name="a11y_collapse_message_text_field">"Minimiziraj tekstno polje poruke"</string>
|
||||
<string name="a11y_delete">"Izbriši"</string>
|
||||
|
|
@ -27,9 +28,12 @@
|
|||
<string name="a11y_pause">"Pauziraj"</string>
|
||||
<string name="a11y_paused_voice_message">"Glasovna poruka, trajanje: %1$s, trenutačno zaustavljeno na: %2$s"</string>
|
||||
<string name="a11y_pin_field">"Polje za PIN"</string>
|
||||
<string name="a11y_pinned_location">"Prikvačena lokacija"</string>
|
||||
<string name="a11y_play">"Reproduciraj"</string>
|
||||
<string name="a11y_playback_speed">"Brzina reprodukcije"</string>
|
||||
<string name="a11y_poll">"Anketa"</string>
|
||||
<string name="a11y_poll_end">"Završena anketa"</string>
|
||||
<string name="a11y_qr_code">"QR kod"</string>
|
||||
<string name="a11y_react_with">"Reagiraj s %1$s"</string>
|
||||
<string name="a11y_react_with_other_emojis">"Reagiraj s drugim emotikonima"</string>
|
||||
<string name="a11y_read_receipts_multiple">"Pročitali %1$s i %2$s"</string>
|
||||
|
|
@ -44,9 +48,12 @@
|
|||
<string name="a11y_remove_reaction_with">"Ukloni reakciju s %1$s"</string>
|
||||
<string name="a11y_room_avatar">"Avatar sobe"</string>
|
||||
<string name="a11y_send_files">"Pošalji datoteke"</string>
|
||||
<string name="a11y_sender_location">"Lokacija pošiljatelja"</string>
|
||||
<string name="a11y_session_verification_time_limited_action_required">"Potrebna je vremenski ograničena radnja, imate jednu minutu za potvrdu"</string>
|
||||
<string name="a11y_show_password">"Prikaži zaporku"</string>
|
||||
<string name="a11y_start_call">"Započni poziv"</string>
|
||||
<string name="a11y_start_video_call">"Započni videopoziv"</string>
|
||||
<string name="a11y_start_voice_call">"Započni glasovni poziv"</string>
|
||||
<string name="a11y_tombstoned_room">"Soba označena za uklanjanje"</string>
|
||||
<string name="a11y_user_avatar">"Korisnički avatar"</string>
|
||||
<string name="a11y_user_menu">"Korisnički izbornik"</string>
|
||||
|
|
@ -58,6 +65,7 @@
|
|||
<string name="a11y_your_avatar">"Vaš avatar"</string>
|
||||
<string name="action_accept">"Prihvati"</string>
|
||||
<string name="action_add_caption">"Dodaj opis"</string>
|
||||
<string name="action_add_existing_rooms">"Dodaj postojeće sobe"</string>
|
||||
<string name="action_add_to_timeline">"Dodaj na vremensku traku"</string>
|
||||
<string name="action_back">"Natrag"</string>
|
||||
<string name="action_call">"Poziv"</string>
|
||||
|
|
@ -76,7 +84,8 @@
|
|||
<string name="action_copy_link_to_message">"Kopiraj poveznicu u poruku"</string>
|
||||
<string name="action_copy_text">"Kopiraj tekst"</string>
|
||||
<string name="action_create">"Stvori"</string>
|
||||
<string name="action_create_room">"Stvori sobu"</string>
|
||||
<string name="action_create_room">"Napravi sobu"</string>
|
||||
<string name="action_create_space">"Stvori prostor"</string>
|
||||
<string name="action_deactivate">"Deaktiviraj"</string>
|
||||
<string name="action_deactivate_account">"Deaktiviraj račun"</string>
|
||||
<string name="action_decline">"Odbij"</string>
|
||||
|
|
@ -93,6 +102,7 @@
|
|||
<string name="action_enable">"Omogući"</string>
|
||||
<string name="action_end_poll">"Završi anketu"</string>
|
||||
<string name="action_enter_pin">"Unesite PIN"</string>
|
||||
<string name="action_explore_public_spaces">"Istražite javne prostore"</string>
|
||||
<string name="action_finish">"Završi"</string>
|
||||
<string name="action_forgot_password">"Zaboravili ste zaporku?"</string>
|
||||
<string name="action_forward">"Proslijedi"</string>
|
||||
|
|
@ -113,6 +123,7 @@
|
|||
<string name="action_leave_space">"Napusti prostor"</string>
|
||||
<string name="action_load_more">"Učitaj više"</string>
|
||||
<string name="action_manage_account">"Upravljanje računom"</string>
|
||||
<string name="action_manage_account_and_devices">"Upravljanje računom i uređajima"</string>
|
||||
<string name="action_manage_devices">"Upravljanje uređajima"</string>
|
||||
<string name="action_manage_rooms">"Upravljaj sobama"</string>
|
||||
<string name="action_message">"Poruka"</string>
|
||||
|
|
@ -152,16 +163,18 @@
|
|||
<string name="action_send_voice_message">"Pošalji glasovnu poruku"</string>
|
||||
<string name="action_share">"Podijeli"</string>
|
||||
<string name="action_share_link">"Podijeli poveznicu"</string>
|
||||
<string name="action_share_live_location">"Dijeljenje lokacije uživo"</string>
|
||||
<string name="action_show">"Prikaži"</string>
|
||||
<string name="action_sign_in_again">"Ponovno se prijavite"</string>
|
||||
<string name="action_signout">"Odjava"</string>
|
||||
<string name="action_signout_anyway">"Svejedno se odjavi"</string>
|
||||
<string name="action_signout">"Ukloni ovaj uređaj"</string>
|
||||
<string name="action_signout_anyway">"Ukloni ovaj uređaj svejedno"</string>
|
||||
<string name="action_skip">"Preskoči"</string>
|
||||
<string name="action_start">"Započni"</string>
|
||||
<string name="action_start_chat">"Započni razgovor"</string>
|
||||
<string name="action_start_over">"Kreni ispočetka"</string>
|
||||
<string name="action_start_verification">"Započni provjeru"</string>
|
||||
<string name="action_static_map_load">"Dodirnite za učitavanje karte"</string>
|
||||
<string name="action_stop">"Zaustavi"</string>
|
||||
<string name="action_take_photo">"Uslikaj"</string>
|
||||
<string name="action_tap_for_options">"Dodirnite za mogućnosti"</string>
|
||||
<string name="action_translate">"Prevedi"</string>
|
||||
|
|
@ -182,6 +195,7 @@
|
|||
<string name="common_advanced_settings">"Napredne postavke"</string>
|
||||
<string name="common_an_image">"slika"</string>
|
||||
<string name="common_analytics">"Analitika"</string>
|
||||
<string name="common_android_fetching_notifications_title">"Sinkronizacija obavijesti…"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_left_room">"Napustili ste sobu"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_session_logged_out">"Odjavljeni ste iz sesije"</string>
|
||||
<string name="common_appearance">"Izgled"</string>
|
||||
|
|
@ -194,6 +208,7 @@
|
|||
<string name="common_copied_to_clipboard">"Kopirano u međuspremnik"</string>
|
||||
<string name="common_copyright">"Autorsko pravo"</string>
|
||||
<string name="common_creating_room">"Stvaranje sobe…"</string>
|
||||
<string name="common_creating_space">"Stvaranje prostora…"</string>
|
||||
<string name="common_current_user_canceled_knock">"Zahtjev je otkazan"</string>
|
||||
<string name="common_current_user_left_room">"Napustio/la je sobu"</string>
|
||||
<string name="common_current_user_left_space">"Napušteni prostor"</string>
|
||||
|
|
@ -214,6 +229,7 @@
|
|||
<string name="common_empty_file">"Prazna datoteka"</string>
|
||||
<string name="common_encryption">"Šifriranje"</string>
|
||||
<string name="common_encryption_enabled">"Šifriranje je omogućeno"</string>
|
||||
<string name="common_ends_at">"Završava u %1$s"</string>
|
||||
<string name="common_enter_your_pin">"Unesite svoj PIN"</string>
|
||||
<string name="common_error">"Pogreška"</string>
|
||||
<string name="common_error_registering_pusher_android">"Došlo je do pogreške; možda nećete primati obavijesti za nove poruke. Riješite problem s obavijestima u postavkama.
|
||||
|
|
@ -240,6 +256,8 @@ Razlog: %1$s ."</string>
|
|||
<string name="common_line_copied_to_clipboard">"Redak je kopiran u međuspremnik"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Poveznica je kopirana u međuspremnik."</string>
|
||||
<string name="common_link_new_device">"Poveži novi uređaj"</string>
|
||||
<string name="common_live_location">"Lokacija uživo"</string>
|
||||
<string name="common_live_location_ended">"Prikaz lokacije uživo je završio"</string>
|
||||
<string name="common_loading">"Učitavanje…"</string>
|
||||
<string name="common_loading_more">"Učitava se još…"</string>
|
||||
<plurals name="common_many_members">
|
||||
|
|
@ -268,6 +286,7 @@ Razlog: %1$s ."</string>
|
|||
<string name="common_offline">"Izvan mreže"</string>
|
||||
<string name="common_open_source_licenses">"Licencije otvorenog koda"</string>
|
||||
<string name="common_or">"ili"</string>
|
||||
<string name="common_other_options">"Ostale opcije"</string>
|
||||
<string name="common_password">"Zaporka"</string>
|
||||
<string name="common_people">"Osobe"</string>
|
||||
<string name="common_permalink">"Stalna poveznica"</string>
|
||||
|
|
@ -286,8 +305,10 @@ Razlog: %1$s ."</string>
|
|||
</plurals>
|
||||
<string name="common_preparing">"Priprema…"</string>
|
||||
<string name="common_privacy_policy">"Pravilnik o zaštiti privatnosti"</string>
|
||||
<string name="common_private">"Privatno"</string>
|
||||
<string name="common_private_room">"Privatna soba"</string>
|
||||
<string name="common_private_space">"Privatni prostor"</string>
|
||||
<string name="common_public">"Javno"</string>
|
||||
<string name="common_public_room">"Javna soba"</string>
|
||||
<string name="common_public_space">"Javni prostor"</string>
|
||||
<string name="common_reaction">"Reakcija"</string>
|
||||
|
|
@ -295,6 +316,7 @@ Razlog: %1$s ."</string>
|
|||
<string name="common_reason">"Razlog"</string>
|
||||
<string name="common_recovery_key">"Ključ za oporavak"</string>
|
||||
<string name="common_refreshing">"Osvježavanje…"</string>
|
||||
<string name="common_removing">"U tijeku je uklanjanje…"</string>
|
||||
<plurals name="common_replies">
|
||||
<item quantity="one">"%1$d odgovor"</item>
|
||||
<item quantity="few">"%1$d odgovora"</item>
|
||||
|
|
@ -305,6 +327,7 @@ Razlog: %1$s ."</string>
|
|||
<string name="common_report_a_problem">"Prijavi problem"</string>
|
||||
<string name="common_report_submitted">"Prijava je podnesena"</string>
|
||||
<string name="common_rich_text_editor">"Uređivač obogaćenog teksta"</string>
|
||||
<string name="common_role">"Uloga"</string>
|
||||
<string name="common_room">"Soba"</string>
|
||||
<string name="common_room_name">"Naziv sobe"</string>
|
||||
<string name="common_room_name_placeholder">"npr. naziv vašeg projekta"</string>
|
||||
|
|
@ -321,6 +344,10 @@ Razlog: %1$s ."</string>
|
|||
<string name="common_security">"Sigurnost"</string>
|
||||
<string name="common_seen_by">"Vidio/la"</string>
|
||||
<string name="common_select_account">"Odaberi račun"</string>
|
||||
<plurals name="common_selected_count">
|
||||
<item quantity="one">"%1$d odabrano"</item>
|
||||
<item quantity="other">"%1$d odabrano"</item>
|
||||
</plurals>
|
||||
<string name="common_send_to">"Pošalji"</string>
|
||||
<string name="common_sending">"Slanje…"</string>
|
||||
<string name="common_sending_failed">"Slanje nije uspjelo"</string>
|
||||
|
|
@ -331,12 +358,15 @@ Razlog: %1$s ."</string>
|
|||
<string name="common_server_url">"URL poslužitelja"</string>
|
||||
<string name="common_settings">"Postavke"</string>
|
||||
<string name="common_share_space">"Podijeli prostor"</string>
|
||||
<string name="common_shared_history">"Novi članovi vide povijest"</string>
|
||||
<string name="common_shared_live_location">"Dijeljena lokacija uživo"</string>
|
||||
<string name="common_shared_location">"Podijeljena lokacija"</string>
|
||||
<string name="common_shared_space">"Zajednički prostor"</string>
|
||||
<string name="common_signing_out">"Odjava je u tijeku"</string>
|
||||
<string name="common_signing_out">"Uklanjanje uređaja"</string>
|
||||
<string name="common_something_went_wrong">"Nešto je pošlo po zlu"</string>
|
||||
<string name="common_something_went_wrong_message">"Naišli smo na problem. Pokušajte ponovno."</string>
|
||||
<string name="common_space">"Prostor"</string>
|
||||
<string name="common_space_members">"Članovi prostora"</string>
|
||||
<string name="common_space_topic_placeholder">"O čemu se radi u ovom prostoru?"</string>
|
||||
<plurals name="common_spaces">
|
||||
<item quantity="one">"%1$d prostor"</item>
|
||||
|
|
@ -346,12 +376,14 @@ Razlog: %1$s ."</string>
|
|||
<string name="common_starting_chat">"Započinjanje razgovora…"</string>
|
||||
<string name="common_sticker">"Naljepnica"</string>
|
||||
<string name="common_success">"Uspjeh"</string>
|
||||
<string name="common_suggested">"Preporučeno"</string>
|
||||
<string name="common_suggestions">"Prijedlozi"</string>
|
||||
<string name="common_syncing">"Sinkronizacija"</string>
|
||||
<string name="common_system">"Sustav"</string>
|
||||
<string name="common_text">"Tekst"</string>
|
||||
<string name="common_third_party_notices">"Obavijesti trećih strana"</string>
|
||||
<string name="common_thread">"Nit"</string>
|
||||
<string name="common_threads">"Niti"</string>
|
||||
<string name="common_topic">"Tema"</string>
|
||||
<string name="common_topic_placeholder">"O čemu je ova soba?"</string>
|
||||
<string name="common_unable_to_decrypt">"Nije moguće dešifrirati"</string>
|
||||
|
|
@ -382,7 +414,11 @@ Razlog: %1$s ."</string>
|
|||
<string name="common_voice_message">"Glasovna poruka"</string>
|
||||
<string name="common_waiting">"Čekanje…"</string>
|
||||
<string name="common_waiting_for_decryption_key">"Čekam ovu poruku"</string>
|
||||
<string name="common_waiting_live_location">"Čekanje lokacije uživo…"</string>
|
||||
<string name="common_world_readable_history">"Svatko može vidjeti povijest"</string>
|
||||
<string name="common_you">"Vi"</string>
|
||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s(%2$s ) je podijelio/la ovu poruku jer niste bili u sobi kada je poslana."</string>
|
||||
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"%1$spodijelio/la je ovu poruku jer nisi bio/la u sobi kada je poslana."</string>
|
||||
<string name="crypto_history_visible">"Ova je soba konfigurirana tako da novi članovi mogu čitati stare poruke. %1$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"Identitet korisnika %1$s je poništen. %2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"Identitet korisnika %1$s %2$s je poništen. %3$s"</string>
|
||||
|
|
@ -390,6 +426,7 @@ Razlog: %1$s ."</string>
|
|||
<string name="crypto_identity_change_profile_pin_violation">"Identitet korisnika %1$s je poništen."</string>
|
||||
<string name="crypto_identity_change_verification_violation_new">"Identitet korisnika %1$s %2$s je poništen. %3$s"</string>
|
||||
<string name="crypto_identity_change_withdraw_verification_action">"Povuci provjeru"</string>
|
||||
<string name="dialog_allow_access">"Dopusti pristup"</string>
|
||||
<string name="dialog_confirm_link_message">"Poveznica %1$s vodi vas na drugo mrežno mjesto %2$s
|
||||
|
||||
Jeste li sigurni da želite nastaviti?"</string>
|
||||
|
|
@ -419,6 +456,7 @@ Jeste li sigurni da želite nastaviti?"</string>
|
|||
<string name="error_failed_locating_user">"%1$s nije mogao pristupiti vašoj lokaciji. Pokušajte ponovno poslije."</string>
|
||||
<string name="error_failed_uploading_voice_message">"Prijenos vaše glasovne poruke nije uspio."</string>
|
||||
<string name="error_invalid_invite">"Soba više ne postoji ili pozivnica više ne vrijedi."</string>
|
||||
<string name="error_location_service_disabled_android">"Omogućite GPS za pristup značajkama temeljenim na lokaciji."</string>
|
||||
<string name="error_message_not_found">"Poruka nije pronađena"</string>
|
||||
<string name="error_missing_location_auth_android">"%1$s nema dopuštenje za pristup vašoj lokaciji. Pristup možete omogućiti u postavkama."</string>
|
||||
<string name="error_missing_location_rationale_android">"%1$s nema dopuštenje za pristup vašoj lokaciji. Omogućite pristup u nastavku."</string>
|
||||
|
|
@ -462,6 +500,7 @@ Jeste li sigurni da želite nastaviti?"</string>
|
|||
<string name="screen_room_event_pill">"Poruka u sobi %1$s"</string>
|
||||
<string name="screen_room_grouped_state_events_expand">"Proširi"</string>
|
||||
<string name="screen_room_grouped_state_events_reduce">"Smanji"</string>
|
||||
<string name="screen_room_live_location_banner">"Dijeljenje lokacije uživo"</string>
|
||||
<string name="screen_room_permalink_same_room_android">"Već gledam ovu sobu!"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s od %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Prikvačene poruke"</string>
|
||||
|
|
@ -474,10 +513,14 @@ Jeste li sigurni da želite nastaviti?"</string>
|
|||
<string name="screen_share_open_google_maps">"Otvori u Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Otvori u OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Podijeli ovu lokaciju"</string>
|
||||
<string name="screen_sharing_location_option_sheet_title">"Opcije dijeljenja"</string>
|
||||
<string name="screen_space_list_description">"Prostori koje ste stvorili ili kojima ste se pridružili."</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_empty_state_title">"Stvorite prostore za organizaciju soba"</string>
|
||||
<string name="screen_space_list_parent_space">"Prostor %1$s"</string>
|
||||
<string name="screen_space_list_title">"Prostori"</string>
|
||||
<string name="screen_static_location_sheet_timestamp_description">"Dijeljeno %1$s"</string>
|
||||
<string name="screen_static_location_sheet_title">"Na karti"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Poruka nije poslana jer je poništen potvrđeni identitet korisnika %1$s."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Poruka nije poslana jer %1$s nije potvrdio sve uređaje."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Poruka nije poslana jer niste potvrdili jedan svoj uređaj ili više njih."</string>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
<string name="a11y_session_verification_time_limited_action_required">"Időkorlátos művelet szükséges, egy perce van az ellenőrzésre"</string>
|
||||
<string name="a11y_show_password">"Jelszó megjelenítése"</string>
|
||||
<string name="a11y_start_call">"Hanghívás indítása"</string>
|
||||
<string name="a11y_start_video_call">"Videohívás indítása"</string>
|
||||
<string name="a11y_start_voice_call">"Hanghívás indítása"</string>
|
||||
<string name="a11y_tombstoned_room">"Elévült szoba"</string>
|
||||
<string name="a11y_user_avatar">"Felhasználói profilkép"</string>
|
||||
|
|
@ -465,15 +466,14 @@ Biztos, hogy folytatja?"</string>
|
|||
<string name="screen_create_poll_options_section_title">"Lehetőségek"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Eltávolítás: %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Beállítások"</string>
|
||||
<string name="screen_live_location_sheet_nobody_sharing">"Senki sem osztja meg a tartózkodási helyét"</string>
|
||||
<string name="screen_live_location_sheet_sharing_live_location">"Élő helymegosztás"</string>
|
||||
<plurals name="screen_live_location_sheet_subtitle">
|
||||
<item quantity="one">"%1$d személy"</item>
|
||||
<item quantity="other">"%1$d személy"</item>
|
||||
</plurals>
|
||||
<string name="screen_live_location_sheet_title">"A térképen"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nem sikerült kiválasztani a médiát, próbálja újra."</string>
|
||||
<string name="screen_missing_key_backup_open_element_classic">"Nyissa meg az Element Classic alkalmazást"</string>
|
||||
<string name="screen_missing_key_backup_step_1">"Nyissa meg az Element Classic alkalmazást az eszközén"</string>
|
||||
<string name="screen_missing_key_backup_step_2_android">"Lépjen a Beállítások > Biztonság és adatvédelem menüponthoz"</string>
|
||||
<string name="screen_missing_key_backup_step_3_android">"A Kriptográfiai kulcsok kezelése részben válassza a Titkosított üzenetek helyreállítása lehetőséget"</string>
|
||||
<string name="screen_missing_key_backup_step_4">"Kövesse az utasításokat a kulcstároló engedélyezéséhez"</string>
|
||||
<string name="screen_missing_key_backup_step_5">"Térjen vissza ide: %1$s"</string>
|
||||
<string name="screen_missing_key_backup_title">"Engedélyezze a kulcstárolást a folytatás előtt ide: %1$s"</string>
|
||||
<string name="screen_onboarding_welcome_back">"Üdvözöljük újra!"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Nyomjon hosszan az üzenetre, és válassza a „%1$s” lehetőséget, hogy itt szerepeljen."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Tűzze ki a fontos üzeneteket, hogy könnyen felfedezhetők legyenek"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
@ -497,6 +497,7 @@ Biztos, hogy folytatja?"</string>
|
|||
<string name="screen_room_event_pill">"Üzenet a következőben: %1$s"</string>
|
||||
<string name="screen_room_grouped_state_events_expand">"Kibontás"</string>
|
||||
<string name="screen_room_grouped_state_events_reduce">"Csökkentés"</string>
|
||||
<string name="screen_room_live_location_banner">"Élő helymegosztás"</string>
|
||||
<string name="screen_room_permalink_same_room_android">"Már ezt a szobát nézi!"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s. / %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s kitűzött üzenet"</string>
|
||||
|
|
|
|||
|
|
@ -468,14 +468,6 @@ Sei sicuro di voler continuare?"</string>
|
|||
<string name="screen_create_poll_remove_accessibility_label">"Rimuovi %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Impostazioni"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Selezione del file multimediale fallita, riprova."</string>
|
||||
<string name="screen_missing_key_backup_open_element_classic">"Apri Element Classic"</string>
|
||||
<string name="screen_missing_key_backup_step_1">"Apri Element Classic sul tuo dispositivo"</string>
|
||||
<string name="screen_missing_key_backup_step_2_android">"Vai su Impostazioni > Sicurezza & privacy"</string>
|
||||
<string name="screen_missing_key_backup_step_3_android">"Nella gestione delle chiavi crittografiche, seleziona Recupero dei messaggi cifrati"</string>
|
||||
<string name="screen_missing_key_backup_step_4">"Segui le istruzioni per abilitare l\'archiviazione delle chiavi"</string>
|
||||
<string name="screen_missing_key_backup_step_5">"Torna a %1$s"</string>
|
||||
<string name="screen_missing_key_backup_title">"Abilita l\'archivio delle chiavi prima di procedere con %1$s"</string>
|
||||
<string name="screen_onboarding_welcome_back">"Bentornato"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Premi su un messaggio e scegli “%1$s” per includerlo qui."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Fissa i messaggi importanti così che possano essere trovati facilmente"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@
|
|||
<string name="common_or">"または"</string>
|
||||
<string name="common_other_options">"他のオプション"</string>
|
||||
<string name="common_password">"パスワード"</string>
|
||||
<string name="common_people">"人"</string>
|
||||
<string name="common_people">"人々"</string>
|
||||
<string name="common_permalink">"固定リンク"</string>
|
||||
<string name="common_permission">"権限"</string>
|
||||
<string name="common_pinned">"ピン留め"</string>
|
||||
|
|
@ -458,16 +458,13 @@
|
|||
<string name="screen_create_poll_options_section_title">"選択肢"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"%1$s を削除"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"設定"</string>
|
||||
<string name="screen_live_location_sheet_nobody_sharing">"誰も位置情報を共有していません"</string>
|
||||
<string name="screen_live_location_sheet_sharing_live_location">"ライブ位置情報を共有しています"</string>
|
||||
<plurals name="screen_live_location_sheet_subtitle">
|
||||
<item quantity="other">"%1$d 人"</item>
|
||||
</plurals>
|
||||
<string name="screen_live_location_sheet_title">"地図上"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"ファイルの選択に失敗しました。再試行してください。"</string>
|
||||
<string name="screen_missing_key_backup_open_element_classic">"Element Classic を開く"</string>
|
||||
<string name="screen_missing_key_backup_step_1">"Element Classic をこの端末で開く"</string>
|
||||
<string name="screen_missing_key_backup_step_2_android">"「設定- セキュリティとプライバシー」に移動します"</string>
|
||||
<string name="screen_missing_key_backup_step_3_android">"暗号鍵の管理から、暗号化されたメッセージの回復を選択します"</string>
|
||||
<string name="screen_missing_key_backup_step_4">"指示に従って、鍵の保管庫を有効化してください"</string>
|
||||
<string name="screen_missing_key_backup_step_5">"%1$s に戻ってください"</string>
|
||||
<string name="screen_missing_key_backup_title">"%1$s に続行する前に、鍵の保管庫を有効化してください"</string>
|
||||
<string name="screen_onboarding_checking_account">"アカウントを確認中"</string>
|
||||
<string name="screen_onboarding_welcome_back">"おかえりなさい"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"メッセージを長押しし \"%1$s\" を選択してください"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"重要なメッセージをピン留めして容易に見つけられるようにします"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
|
|
@ -460,14 +460,6 @@
|
|||
<string name="screen_create_poll_remove_accessibility_label">"%1$s 제거"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"설정"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"미디어 선택에 실패했습니다. 다시 시도해 주세요."</string>
|
||||
<string name="screen_missing_key_backup_open_element_classic">"Element Classic 열기"</string>
|
||||
<string name="screen_missing_key_backup_step_1">"기기에서 Element Classic 앱을 열어 주세요"</string>
|
||||
<string name="screen_missing_key_backup_step_2_android">"설정 > 보안 및 개인정보 보호로 이동하세요"</string>
|
||||
<string name="screen_missing_key_backup_step_3_android">"암호화 키 관리에서 \'암호화된 메시지 복구\'를 선택하세요"</string>
|
||||
<string name="screen_missing_key_backup_step_4">"안내에 따라 키 저장소를 활성화해 주세요"</string>
|
||||
<string name="screen_missing_key_backup_step_5">"%1$s(으)로 돌아가기"</string>
|
||||
<string name="screen_missing_key_backup_title">"%1$s(으)로 진행하기 전에 키 저장소를 활성화해 주세요."</string>
|
||||
<string name="screen_onboarding_welcome_back">"다시 오신 것을 환영합니다"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"메시지를 누르고 \"%1$s\" 를 선택하여 여기에 포함합니다."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"중요한 메시지를 고정하여 쉽게 찾을 수 있도록 합니다"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
|
|
@ -170,7 +170,6 @@
|
|||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Papurtykite, kad praneštumėte apie klaidą"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nepavyko pasirinkti laikmenos, pabandykite dar kartą."</string>
|
||||
<string name="screen_onboarding_welcome_back">"Sveiki sugrįžę"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nepavyko gauti naudotojo išsamios informacijos."</string>
|
||||
<string name="screen_share_location_title">"Bendrinti vietą"</string>
|
||||
|
|
|
|||
|
|
@ -455,7 +455,6 @@ Er du sikker på at du vil fortsette?"</string>
|
|||
<string name="screen_create_poll_remove_accessibility_label">"Fjern %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Innstillinger"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Kunne ikke velge medium, prøv igjen."</string>
|
||||
<string name="screen_onboarding_welcome_back">"Velkommen tilbake"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Trykk på en melding og velg “%1$s” for å inkludere her."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Fest viktige meldinger slik at de lett kan ses"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
|
|
@ -476,15 +476,15 @@
|
|||
<string name="screen_create_poll_options_section_title">"Параметры"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Удалить %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Настройки"</string>
|
||||
<string name="screen_live_location_sheet_nobody_sharing">"Никто не делится своим местоположением"</string>
|
||||
<string name="screen_live_location_sheet_sharing_live_location">"Местоположение отправляется в реальном времени"</string>
|
||||
<plurals name="screen_live_location_sheet_subtitle">
|
||||
<item quantity="one">"%1$d человек"</item>
|
||||
<item quantity="few">"%1$d человек"</item>
|
||||
<item quantity="many">"%1$d людей"</item>
|
||||
</plurals>
|
||||
<string name="screen_live_location_sheet_title">"На карте"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать медиа, попробуйте еще раз."</string>
|
||||
<string name="screen_missing_key_backup_open_element_classic">"Открыть Element Classic"</string>
|
||||
<string name="screen_missing_key_backup_step_1">"Откройте Element Classic на своем устройстве."</string>
|
||||
<string name="screen_missing_key_backup_step_2_android">"Перейдите в Настройки > Безопасность и конфиденциальность"</string>
|
||||
<string name="screen_missing_key_backup_step_3_android">"В разделе «Управление криптографическими ключами» выбери «Восстановление зашифрованных сообщений»"</string>
|
||||
<string name="screen_missing_key_backup_step_4">"Следуйте инструкциям, чтобы активировать хранилище ключей"</string>
|
||||
<string name="screen_missing_key_backup_step_5">"Вернитесь к %1$s"</string>
|
||||
<string name="screen_missing_key_backup_title">"Перед продолжением активируйте хранилище ключей %1$s"</string>
|
||||
<string name="screen_onboarding_welcome_back">"С возвращением"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Нажмите на сообщение и выберите «%1$s», чтобы добавить его сюда."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Закрепите важные сообщения, чтобы их можно было легко найти"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_add_reaction">"Reaksiya qoʻyish: %1$s"</string>
|
||||
<string name="a11y_address">"Manzil"</string>
|
||||
<string name="a11y_avatar">"Avatar"</string>
|
||||
<string name="a11y_collapse_message_text_field">"Xabar matni maydonini kichraytirish"</string>
|
||||
<string name="a11y_delete">"Oʻchirish"</string>
|
||||
|
|
@ -26,9 +27,12 @@
|
|||
<string name="a11y_pause">"Pauza"</string>
|
||||
<string name="a11y_paused_voice_message">"Ovoz xabar, davomiyligi: %1$s, joriy holati: %2$s"</string>
|
||||
<string name="a11y_pin_field">"PIN-kod maydoni"</string>
|
||||
<string name="a11y_pinned_location">"Belgilangan joylashuv"</string>
|
||||
<string name="a11y_play">"O\'ynang"</string>
|
||||
<string name="a11y_playback_speed">"Ijro tezligi"</string>
|
||||
<string name="a11y_poll">"So\'ro\'vnoma"</string>
|
||||
<string name="a11y_poll_end">"So‘rovnoma yakunlandi"</string>
|
||||
<string name="a11y_qr_code">"QR kodi"</string>
|
||||
<string name="a11y_react_with">"%1$s bilan munosabat bildiring"</string>
|
||||
<string name="a11y_react_with_other_emojis">"Boshqa hisbelgilar bilan munosabat bildiring"</string>
|
||||
<string name="a11y_read_receipts_multiple">"%1$s va %2$s bilan oʻqish"</string>
|
||||
|
|
@ -42,9 +46,12 @@
|
|||
<string name="a11y_remove_reaction_with">"%1$s bilan reaktsiyani olib tashlang"</string>
|
||||
<string name="a11y_room_avatar">"Xona avatari"</string>
|
||||
<string name="a11y_send_files">"Fayllarni yuborish"</string>
|
||||
<string name="a11y_sender_location">"Yuboruvchining joylashuvi"</string>
|
||||
<string name="a11y_session_verification_time_limited_action_required">"Amal bajarish vaqti cheklangan, tasdiqlash uchun bir daqiqa vaqtingiz bor"</string>
|
||||
<string name="a11y_show_password">"Parolni ko\'rsatish"</string>
|
||||
<string name="a11y_start_call">"Qoʻngʻiroqni boshlash"</string>
|
||||
<string name="a11y_start_video_call">"Video chaqiruvni boshlash"</string>
|
||||
<string name="a11y_start_voice_call">"Ovozli qo‘ng‘iroq qilish"</string>
|
||||
<string name="a11y_tombstoned_room">"Arxivlangan xona"</string>
|
||||
<string name="a11y_user_avatar">"Foydalanuvchi avatari"</string>
|
||||
<string name="a11y_user_menu">"Foydalanuvchi menyusi"</string>
|
||||
|
|
@ -56,6 +63,7 @@
|
|||
<string name="a11y_your_avatar">"Sizning avataringiz"</string>
|
||||
<string name="action_accept">"Qabul qiling"</string>
|
||||
<string name="action_add_caption">"Sarlavha qo\'shing"</string>
|
||||
<string name="action_add_existing_rooms">"Mavjud xonalarni qo‘shish"</string>
|
||||
<string name="action_add_to_timeline">"Vaqt jadvaliga qo\'shing"</string>
|
||||
<string name="action_back">"Orqaga"</string>
|
||||
<string name="action_call">"Qoʻngʻiroq"</string>
|
||||
|
|
@ -75,6 +83,7 @@
|
|||
<string name="action_copy_text">"Matnni nusxalash"</string>
|
||||
<string name="action_create">"Yaratmoq"</string>
|
||||
<string name="action_create_room">"Xonani yaratish"</string>
|
||||
<string name="action_create_space">"Maydon yaratish"</string>
|
||||
<string name="action_deactivate">"Faolsizlantirish"</string>
|
||||
<string name="action_deactivate_account">"Hisobni faolsizlantirish"</string>
|
||||
<string name="action_decline">"Rad etish"</string>
|
||||
|
|
@ -91,6 +100,7 @@
|
|||
<string name="action_enable">"Yoqish"</string>
|
||||
<string name="action_end_poll">"So‘rovnomani tugatish"</string>
|
||||
<string name="action_enter_pin">"PIN kodni kiriting"</string>
|
||||
<string name="action_explore_public_spaces">"Jamoat maydonlari o‘rganing"</string>
|
||||
<string name="action_finish">"Tugatish"</string>
|
||||
<string name="action_forgot_password">"Parolni unutdingizmi?"</string>
|
||||
<string name="action_forward">"Oldinga"</string>
|
||||
|
|
@ -111,6 +121,7 @@
|
|||
<string name="action_leave_space">"Maydondan chiqish"</string>
|
||||
<string name="action_load_more">"Ko\'proq yuklash"</string>
|
||||
<string name="action_manage_account">"Hisobni boshqarish"</string>
|
||||
<string name="action_manage_account_and_devices">"Hisob va qurilmalarni boshqarish"</string>
|
||||
<string name="action_manage_devices">"Qurilmalarni boshqarish"</string>
|
||||
<string name="action_manage_rooms">"Xonalarni boshqarish"</string>
|
||||
<string name="action_message">"Xabar"</string>
|
||||
|
|
@ -150,18 +161,21 @@
|
|||
<string name="action_send_voice_message">"Ovozli xabar yuborish"</string>
|
||||
<string name="action_share">"Ulashish"</string>
|
||||
<string name="action_share_link">"Havolani ulashing"</string>
|
||||
<string name="action_share_live_location">"Jonli joylashuvni ulashish"</string>
|
||||
<string name="action_show">"Koʻrsatish"</string>
|
||||
<string name="action_sign_in_again">"Qaytadan kiring"</string>
|
||||
<string name="action_signout">"Tizimdan chiqish"</string>
|
||||
<string name="action_signout_anyway">"Baribir tizimdan chiqing"</string>
|
||||
<string name="action_signout">"Bu qurilmani olib tashlash"</string>
|
||||
<string name="action_signout_anyway">"Bu qurilma baribir olib tashlansin"</string>
|
||||
<string name="action_skip">"Oʻtkazib yuborish"</string>
|
||||
<string name="action_start">"Boshlash"</string>
|
||||
<string name="action_start_chat">"Suhbatni boshlash"</string>
|
||||
<string name="action_start_over">"Qaytadan boshlang"</string>
|
||||
<string name="action_start_verification">"Tasdiqlashni boshlang"</string>
|
||||
<string name="action_static_map_load">"Xaritani yuklash uchun bosing"</string>
|
||||
<string name="action_stop">"To‘xtatish"</string>
|
||||
<string name="action_take_photo">"Rasmga olmoq"</string>
|
||||
<string name="action_tap_for_options">"Variantlar uchun bosing"</string>
|
||||
<string name="action_translate">"Tarjima"</string>
|
||||
<string name="action_try_again">"Qayta urinib ko\'ring"</string>
|
||||
<string name="action_unpin">"Olib tashlash"</string>
|
||||
<string name="action_view">"Ko\'rish"</string>
|
||||
|
|
@ -179,6 +193,7 @@
|
|||
<string name="common_advanced_settings">"Kengaytirilgan sozlamalar"</string>
|
||||
<string name="common_an_image">"rasm"</string>
|
||||
<string name="common_analytics">"Analitika"</string>
|
||||
<string name="common_android_fetching_notifications_title">"Bildirishnomalar sinxronlanmoqda…"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_left_room">"Siz xonani tark etdingiz"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_session_logged_out">"Siz sessiyadan chiqdingiz"</string>
|
||||
<string name="common_appearance">"Ko\'rinish"</string>
|
||||
|
|
@ -191,6 +206,7 @@
|
|||
<string name="common_copied_to_clipboard">"Buferga nusxa koʻchirildi"</string>
|
||||
<string name="common_copyright">"Mualliflik huquqi"</string>
|
||||
<string name="common_creating_room">"Xona yaratilmoqda…"</string>
|
||||
<string name="common_creating_space">"Joy yaratilmoqda…"</string>
|
||||
<string name="common_current_user_canceled_knock">"So\'rov bekor qilindi"</string>
|
||||
<string name="common_current_user_left_room">"Xonani tark etdi"</string>
|
||||
<string name="common_current_user_left_space">"Tar etilgan maydon"</string>
|
||||
|
|
@ -211,6 +227,7 @@
|
|||
<string name="common_empty_file">"Bo\'sh fayl"</string>
|
||||
<string name="common_encryption">"Shifrlash"</string>
|
||||
<string name="common_encryption_enabled">"Shifrlash yoqilgan"</string>
|
||||
<string name="common_ends_at">"Tugaydi: %1$s"</string>
|
||||
<string name="common_enter_your_pin">"PIN kodini kiriting"</string>
|
||||
<string name="common_error">"Xato"</string>
|
||||
<string name="common_error_registering_pusher_android">"Xato yuz berdi, siz yangi xabarlar uchun bildirishnomalarni olmasligingiz mumkin. Iltimos, sozlamalardan bildirishnomalarni bartaraf eting.
|
||||
|
|
@ -237,6 +254,8 @@ Sababi:%1$s."</string>
|
|||
<string name="common_line_copied_to_clipboard">"Satr vaqtinchalik xotiraga nusxalandi"</string>
|
||||
<string name="common_link_copied_to_clipboard">"Havola vaqtinchalik xotiraga nusxalandi"</string>
|
||||
<string name="common_link_new_device">"Yangi qurilmani ulang"</string>
|
||||
<string name="common_live_location">"Jonli joylashuv"</string>
|
||||
<string name="common_live_location_ended">"Jonli joylashuv tugadi"</string>
|
||||
<string name="common_loading">"Yuklanmoqda…"</string>
|
||||
<string name="common_loading_more">"Batafsil yuklanmoqda…"</string>
|
||||
<plurals name="common_many_members">
|
||||
|
|
@ -263,6 +282,7 @@ Sababi:%1$s."</string>
|
|||
<string name="common_offline">"Oflayn"</string>
|
||||
<string name="common_open_source_licenses">"Ochiq kodli litsenziyalar"</string>
|
||||
<string name="common_or">"yoki"</string>
|
||||
<string name="common_other_options">"Boshqa variantlar"</string>
|
||||
<string name="common_password">"Parol"</string>
|
||||
<string name="common_people">"Odamlar"</string>
|
||||
<string name="common_permalink">"Doimiy havola"</string>
|
||||
|
|
@ -280,8 +300,10 @@ Sababi:%1$s."</string>
|
|||
</plurals>
|
||||
<string name="common_preparing">"Tayyorlanmoqda…"</string>
|
||||
<string name="common_privacy_policy">"Maxfiylik siyosati"</string>
|
||||
<string name="common_private">"Maxfiy"</string>
|
||||
<string name="common_private_room">"Shaxsiy xona"</string>
|
||||
<string name="common_private_space">"Shaxsiy guruh"</string>
|
||||
<string name="common_public">"Ommaviy"</string>
|
||||
<string name="common_public_room">"Jamoat xonasi"</string>
|
||||
<string name="common_public_space">"Jamoat guruhi"</string>
|
||||
<string name="common_reaction">"Reaktsiya"</string>
|
||||
|
|
@ -289,6 +311,7 @@ Sababi:%1$s."</string>
|
|||
<string name="common_reason">"Sabab"</string>
|
||||
<string name="common_recovery_key">"Qayta tiklash kaliti"</string>
|
||||
<string name="common_refreshing">"Yangilanmoqda…"</string>
|
||||
<string name="common_removing">"Olib tashlanmoqda…"</string>
|
||||
<plurals name="common_replies">
|
||||
<item quantity="one">"%1$d ta javob"</item>
|
||||
<item quantity="other">"%1$d ta javob"</item>
|
||||
|
|
@ -298,6 +321,7 @@ Sababi:%1$s."</string>
|
|||
<string name="common_report_a_problem">"Muammo haqida xabar bering"</string>
|
||||
<string name="common_report_submitted">"Hisobot topshirildi"</string>
|
||||
<string name="common_rich_text_editor">"Boy matn muharriri"</string>
|
||||
<string name="common_role">"Rol"</string>
|
||||
<string name="common_room">"Xona"</string>
|
||||
<string name="common_room_name">"Xona nomi"</string>
|
||||
<string name="common_room_name_placeholder">"masalan, loyihangiz nomi"</string>
|
||||
|
|
@ -313,6 +337,10 @@ Sababi:%1$s."</string>
|
|||
<string name="common_security">"Xavfsizlik"</string>
|
||||
<string name="common_seen_by">"Tomonidan koʻrilgan"</string>
|
||||
<string name="common_select_account">"Hisobni tanlang"</string>
|
||||
<plurals name="common_selected_count">
|
||||
<item quantity="one">"%1$d ta tanlandi"</item>
|
||||
<item quantity="other">"%1$d ta tanlandi"</item>
|
||||
</plurals>
|
||||
<string name="common_send_to">"Yubirish"</string>
|
||||
<string name="common_sending">"Yuborilmoqda…"</string>
|
||||
<string name="common_sending_failed">"Yuborilmadi"</string>
|
||||
|
|
@ -323,12 +351,15 @@ Sababi:%1$s."</string>
|
|||
<string name="common_server_url">"Server URL manzili"</string>
|
||||
<string name="common_settings">"Sozlamalar"</string>
|
||||
<string name="common_share_space">"Maydonni ulashish"</string>
|
||||
<string name="common_shared_history">"Yangi a’zolar tarixni ko‘radi"</string>
|
||||
<string name="common_shared_live_location">"Ulashilgan jonli joylashuv"</string>
|
||||
<string name="common_shared_location">"Joylashuvi ulashildi"</string>
|
||||
<string name="common_shared_space">"Umumiy maydon"</string>
|
||||
<string name="common_signing_out">"Chiqish"</string>
|
||||
<string name="common_signing_out">"Qurilma olib tashlanmoqda"</string>
|
||||
<string name="common_something_went_wrong">"Nimadir xato ketdi"</string>
|
||||
<string name="common_something_went_wrong_message">"Muammoga duch keldik. Iltimos, qayta urinib koʻring."</string>
|
||||
<string name="common_space">"Maydon"</string>
|
||||
<string name="common_space_members">"Maydon a’zolari"</string>
|
||||
<string name="common_space_topic_placeholder">"Bu maydon nima haqida?"</string>
|
||||
<plurals name="common_spaces">
|
||||
<item quantity="one">"%1$d Maydon"</item>
|
||||
|
|
@ -337,12 +368,14 @@ Sababi:%1$s."</string>
|
|||
<string name="common_starting_chat">"Chat boshlanmoqda…"</string>
|
||||
<string name="common_sticker">"Stiker"</string>
|
||||
<string name="common_success">"Muvaffaqiyat"</string>
|
||||
<string name="common_suggested">"Tavsiya etilgan"</string>
|
||||
<string name="common_suggestions">"Tavsiyalar"</string>
|
||||
<string name="common_syncing">"Sinxronlash"</string>
|
||||
<string name="common_system">"Tizim"</string>
|
||||
<string name="common_text">"Matn"</string>
|
||||
<string name="common_third_party_notices">"Uchinchi tomon bildirishnomalari"</string>
|
||||
<string name="common_thread">"Ip"</string>
|
||||
<string name="common_threads">"Mavzular"</string>
|
||||
<string name="common_topic">"Mavzu"</string>
|
||||
<string name="common_topic_placeholder">"Bu xona nima haqida?"</string>
|
||||
<string name="common_unable_to_decrypt">"Shifrni ochish imkonsiz"</string>
|
||||
|
|
@ -373,14 +406,18 @@ Sababi:%1$s."</string>
|
|||
<string name="common_voice_message">"Ovozli xabar"</string>
|
||||
<string name="common_waiting">"Kutilmoqda…"</string>
|
||||
<string name="common_waiting_for_decryption_key">"Ushbu xabarni kutilmoqda"</string>
|
||||
<string name="common_waiting_live_location">"Jonli joylashuv kutilmoqda…"</string>
|
||||
<string name="common_world_readable_history">"Tarixni hamma ko‘rishi mumkin"</string>
|
||||
<string name="common_you">"Siz"</string>
|
||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) bu xabarni ulashdi, chunki u yuborilganda siz xonada emas edingiz."</string>
|
||||
<string name="crypto_history_visible">"Siz yuborgan xabarlar bu xonaga taklif qilingan yangi a’zolarga ulashiladi. %1$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"%1$sning shaxsi qayta tiklandi.%2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"%1$sʼning %2$s shaxsiy ma’lumotlari qayta tiklandi.%3$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"%1$sning raqamli identifikatori qayta tiklandi.%2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"%1$sning%2$s raqamli identifikatsiya qayta tiklandi.%3$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s )"</string>
|
||||
<string name="crypto_identity_change_profile_pin_violation">"%1$sning shaxsi qayta tiklandi."</string>
|
||||
<string name="crypto_identity_change_verification_violation_new">"%1$sʼning %2$s shaxsiy ma’lumotlari qayta o‘rnatildi.%3$s"</string>
|
||||
<string name="crypto_identity_change_profile_pin_violation">"%1$s raqamli identifikatori asliga qaytarildi."</string>
|
||||
<string name="crypto_identity_change_verification_violation_new">"%1$sning%2$s raqamli identifikatsiya qayta tiklandi.%3$s"</string>
|
||||
<string name="crypto_identity_change_withdraw_verification_action">"Tasdiqlashni bekor qilish"</string>
|
||||
<string name="dialog_allow_access">"Ruxsat berish"</string>
|
||||
<string name="dialog_confirm_link_message">"%1$s havolasi sizni boshqa %2$s saytiga olib boradi
|
||||
|
||||
Davom etasizmi?"</string>
|
||||
|
|
@ -410,6 +447,7 @@ Davom etasizmi?"</string>
|
|||
<string name="error_failed_locating_user">"%1$sjoylashuvingizga kira olmadi. Iltimos keyinroq qayta urinib ko\'ring."</string>
|
||||
<string name="error_failed_uploading_voice_message">"Ovozli xabaringizni yuklashda xatolik roʻy berdi."</string>
|
||||
<string name="error_invalid_invite">"Xona endi mavjud emas yoki taklif yaroqsiz."</string>
|
||||
<string name="error_location_service_disabled_android">"Joylashuvga asoslangan funksiyalardan foydalanish uchun GPS funksiyasini yoqing."</string>
|
||||
<string name="error_message_not_found">"Xabar topilmadi"</string>
|
||||
<string name="error_missing_location_auth_android">"%1$sjoylashuvingizga kirishga ruxsati yo\'q. Sozlamalar orqali kirishni yoqishingiz mumkin."</string>
|
||||
<string name="error_missing_location_rationale_android">"%1$sjoylashuvingizga kirishga ruxsati yo\'q. Quyida kirishni yoqing."</string>
|
||||
|
|
@ -428,6 +466,7 @@ Davom etasizmi?"</string>
|
|||
<string name="screen_create_poll_options_section_title">"Parametrlar"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"%1$sni olib tashlash"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Sozlamalar"</string>
|
||||
<string name="screen_live_location_sheet_nobody_sharing">"Hech kim joylashuvini ulashmayapti"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Media tanlash jarayonida xatolik yuz berdi, qayta urinib ko\'ring"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Xabarni bosib, bu yerga kiritish uchun \"%1$s\"-ni tanlang."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Muhim xabarlarni osongina topish uchun qadang"</string>
|
||||
|
|
@ -436,11 +475,11 @@ Davom etasizmi?"</string>
|
|||
<item quantity="other">"%1$d ta qadalgan xabar"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Qadalgan xabarlar"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"Shaxsingizni qayta o‘rnatish uchun %1$s hisobingizga kirishingiz kerak. Shundan so‘ng, avtomatik ravishda ilovaga qaytarilasiz."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Tasdiqlanmadimi? Shaxsingizni tiklash uchun hisobingizga kiring."</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"Raqamli identifikatoringizni tiklash uchun %1$s hisobingizga kirmoqchisiz. Shundan keyin ilovaga qaytarilasiz."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Tasdiqlay olmayapsizmi? Raqamli identifikatorni tiklash uchun hisobingizga kiring."</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"Tasdiqlashni olib tashlang va yuboring"</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_subtitle">"Siz tasdiqlashni bekor qilib, bu xabarni baribir yuborishingiz yoki hozircha to‘xtatib, %1$sʼni qayta tasdiqlagandan so‘ng keyinroq yana urinib ko‘rishingiz mumkin."</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_title">"%1$sning tasdiqlangan shaxsiy ma’lumotlari qayta o‘rnatilganligi tufayli xabaringiz jo‘natilmadi"</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_title">"Xabaringiz yuborilmadi, chunki %1$sning tasdiqlangan raqamli identifikatori asliga qaytarildi"</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"Baribir xabar yuborilsin"</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_subtitle">"%1$s tasdiqlanmagan bir yoki bir nechta qurilmadan foydalanmoqda. Siz xabarni baribir yuborishingiz mumkin yoki hozircha bekor qilib, %2$s barcha qurilmalarini tasdiqlagunga qadar kutib, keyinroq qayta urinishingiz mumkin."</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_title">"%1$s barcha qurilmalarni tasdiqlamagani uchun xabaringiz yuborilmadi"</string>
|
||||
|
|
@ -452,6 +491,7 @@ Davom etasizmi?"</string>
|
|||
<string name="screen_room_event_pill">"Xabar %1$sda"</string>
|
||||
<string name="screen_room_grouped_state_events_expand">"Kengaytirish"</string>
|
||||
<string name="screen_room_grouped_state_events_reduce">"Kamaytirish"</string>
|
||||
<string name="screen_room_live_location_banner">"Jonli joylashuvni ulashish"</string>
|
||||
<string name="screen_room_permalink_same_room_android">"Bu xona allaqachon ko‘rilmoqda!"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$sʼdan %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s ta qadalgan xabar"</string>
|
||||
|
|
@ -464,11 +504,15 @@ Davom etasizmi?"</string>
|
|||
<string name="screen_share_open_google_maps">"Google Mapsda oching"</string>
|
||||
<string name="screen_share_open_osm_maps">"OpenStreetMapda oching"</string>
|
||||
<string name="screen_share_this_location_action">"Bu joylashuvni ulashing"</string>
|
||||
<string name="screen_sharing_location_option_sheet_title">"Ulashish parametrlari"</string>
|
||||
<string name="screen_space_list_description">"Siz yaratgan yoki qo‘shilgan maydonlar."</string>
|
||||
<string name="screen_space_list_details">"%1$s•%2$s"</string>
|
||||
<string name="screen_space_list_empty_state_title">"Xonalarni tartibga solish uchun maydon yarating"</string>
|
||||
<string name="screen_space_list_parent_space">"%1$s ta maydon"</string>
|
||||
<string name="screen_space_list_title">"Maydonlar"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Xabar yuborilmadi, chunki %1$sʼning tasdiqlangan identifikatori asliga qaytarildi."</string>
|
||||
<string name="screen_static_location_sheet_timestamp_description">"%1$s ulashildi"</string>
|
||||
<string name="screen_static_location_sheet_title">"Xaritada"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Xabar yuborilmadi, chunki%1$s ning tasdiqlangan raqamli identifikatsiyasi qayta tiklandi."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Xabar yuborilmadi, chunki %1$s barcha qurilmalarni tasdiqlamagan."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Xabaringiz yuborilmadi, chunki siz bir yoki bir nechta qurilmangizni tasdiqlamagan ekansiz."</string>
|
||||
<string name="screen_view_location_title">"Joylashuv"</string>
|
||||
|
|
@ -478,5 +522,5 @@ Davom etasizmi?"</string>
|
|||
<string name="timeline_decryption_failure_historical_event_unverified_device">"Tarixiy xabarlarga kirish uchun bu qurilmani tasdiqlashingiz kerak"</string>
|
||||
<string name="timeline_decryption_failure_historical_event_user_not_joined">"Sizni ushbu xabarga ruxsatingiz yoʻq"</string>
|
||||
<string name="timeline_decryption_failure_unable_to_decrypt">"Xabarni shifrini ochib bo‘lmadi"</string>
|
||||
<string name="timeline_decryption_failure_withheld_unverified">"Bu xabar bloklandi, chunki siz qurilmangizni tasdiqlamadingiz yoki yuboruvchi shaxsingizni tasdiqlashi kerak bo‘lgani sababli bloklandi"</string>
|
||||
<string name="timeline_decryption_failure_withheld_unverified">"Qurilmangizni tasdiqlamaganingiz yoki yuboruvchi raqamli shaxsingizni tasdiqlashi kerakligi sababli bu xabar bloklandi."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<string name="a11y_collapse_message_text_field">"Thu nhỏ ô nhập tin nhắn"</string>
|
||||
<string name="a11y_delete">"Xóa"</string>
|
||||
<plurals name="a11y_digits_entered">
|
||||
<item quantity="other">"Đã nhập %1$d chữ số"</item>
|
||||
<item quantity="other">"%1$d các chữ số đã nhập"</item>
|
||||
</plurals>
|
||||
<string name="a11y_edit_avatar">"Đổi ảnh đại diện"</string>
|
||||
<string name="a11y_edit_room_address_hint">"Đường dẫn đầy đủ của phòng là %1$s"</string>
|
||||
|
|
@ -47,7 +47,8 @@
|
|||
<string name="a11y_sender_location">"Vị trí người gửi"</string>
|
||||
<string name="a11y_session_verification_time_limited_action_required">"Yêu cầu hành động có giới hạn thời gian, bạn có một phút để xác minh"</string>
|
||||
<string name="a11y_show_password">"Hiện mật khẩu"</string>
|
||||
<string name="a11y_start_call">"Gọi"</string>
|
||||
<string name="a11y_start_call">"Bắt đầu cuộc gọi"</string>
|
||||
<string name="a11y_start_video_call">"Bắt đầu cuộc gọi video"</string>
|
||||
<string name="a11y_start_voice_call">"Bắt đầu cuộc gọi thoại"</string>
|
||||
<string name="a11y_tombstoned_room">"Phòng Tombstone"</string>
|
||||
<string name="a11y_user_avatar">"Ảnh đại diện của người dùng"</string>
|
||||
|
|
@ -270,13 +271,20 @@ Lý do: %1$s ."</string>
|
|||
<string name="common_mute">"Tắt tiếng"</string>
|
||||
<string name="common_name">"Tên"</string>
|
||||
<string name="common_no_results">"Không có kết quả"</string>
|
||||
<string name="common_no_room_name">"Không có tên phòng"</string>
|
||||
<string name="common_no_space_name">"Không có tên space"</string>
|
||||
<string name="common_not_encrypted">"Không được mã hóa"</string>
|
||||
<string name="common_offline">"Ngoại tuyến"</string>
|
||||
<string name="common_open_source_licenses">"Giấy phép mã nguồn mở"</string>
|
||||
<string name="common_or">"hoặc"</string>
|
||||
<string name="common_other_options">"Các lựa chọn khác"</string>
|
||||
<string name="common_password">"Mật khẩu"</string>
|
||||
<string name="common_people">"Danh bạ"</string>
|
||||
<string name="common_permalink">"Liên kết cố định"</string>
|
||||
<string name="common_permission">"Quyền truy cập"</string>
|
||||
<string name="common_pinned">"Đã ghim"</string>
|
||||
<string name="common_please_check_internet_connection">"Vui lòng kiểm tra kết nối internet của bạn."</string>
|
||||
<string name="common_please_wait">"Vui lòng chờ…"</string>
|
||||
<string name="common_poll_end_confirmation">"Bạn có chắc chắn muốn kết thúc cuộc thăm dò này không?"</string>
|
||||
<string name="common_poll_summary">"Khảo sát: %1$s"</string>
|
||||
<string name="common_poll_total_votes">"Tổng số phiếu: %1$s"</string>
|
||||
|
|
@ -284,20 +292,35 @@ Lý do: %1$s ."</string>
|
|||
<plurals name="common_poll_votes_count">
|
||||
<item quantity="other">"%d lượt bình chọn"</item>
|
||||
</plurals>
|
||||
<string name="common_preparing">"Đang chuẩn bị…"</string>
|
||||
<string name="common_privacy_policy">"Chính sách bảo mật"</string>
|
||||
<string name="common_private">"Riêng tư"</string>
|
||||
<string name="common_private_room">"Phòng riêng tư"</string>
|
||||
<string name="common_private_space">"Không gian riêng tư"</string>
|
||||
<string name="common_public">"Công cộng"</string>
|
||||
<string name="common_public_room">"Phòng công cộng"</string>
|
||||
<string name="common_public_space">"Không gian công cộng"</string>
|
||||
<string name="common_reaction">"Biểu cảm"</string>
|
||||
<string name="common_reactions">"Cảm xúc"</string>
|
||||
<string name="common_reason">"Lý do"</string>
|
||||
<string name="common_recovery_key">"Khóa khôi phục."</string>
|
||||
<string name="common_refreshing">"Đang làm mới…"</string>
|
||||
<string name="common_removing">"Đang xóa…"</string>
|
||||
<plurals name="common_replies">
|
||||
<item quantity="other">"%1$d trả lời"</item>
|
||||
</plurals>
|
||||
<string name="common_replying_to">"Đang trả lời cho %1$s"</string>
|
||||
<string name="common_report_a_bug">"Báo cáo lỗi"</string>
|
||||
<string name="common_report_a_problem">"Báo cáo sự cố"</string>
|
||||
<string name="common_report_submitted">"Đã gửi báo cáo"</string>
|
||||
<string name="common_rich_text_editor">"Trình soạn thảo văn bản nâng cao"</string>
|
||||
<string name="common_role">"Vai trò"</string>
|
||||
<string name="common_room">"Phòng"</string>
|
||||
<string name="common_room_name">"Tên phòng"</string>
|
||||
<string name="common_room_name_placeholder">"ví dụ: tên dự án của bạn"</string>
|
||||
<plurals name="common_rooms">
|
||||
<item quantity="other">"%1$d Phòng"</item>
|
||||
</plurals>
|
||||
<string name="common_saved_changes">"Đã lưu thay đổi"</string>
|
||||
<string name="common_saving">"Đang lưu"</string>
|
||||
<string name="common_screen_lock">"Khóa màn hình"</string>
|
||||
|
|
@ -305,6 +328,11 @@ Lý do: %1$s ."</string>
|
|||
<string name="common_search_results">"Kết quả tìm kiếm"</string>
|
||||
<string name="common_security">"Bảo mật"</string>
|
||||
<string name="common_seen_by">"Được xem bởi"</string>
|
||||
<string name="common_select_account">"Chọn tài khoản"</string>
|
||||
<plurals name="common_selected_count">
|
||||
<item quantity="other">"%1$d đã chọn"</item>
|
||||
</plurals>
|
||||
<string name="common_send_to">"Gửi đến"</string>
|
||||
<string name="common_sending">"Đang gửi…"</string>
|
||||
<string name="common_sending_failed">"Không gửi được"</string>
|
||||
<string name="common_sent">"Đã gửi"</string>
|
||||
|
|
@ -323,6 +351,9 @@ Lý do: %1$s ."</string>
|
|||
<string name="common_space">"Không gian"</string>
|
||||
<string name="common_space_members">"Thành viên không gian"</string>
|
||||
<string name="common_space_topic_placeholder">"Không gian này dùng để làm gì?"</string>
|
||||
<plurals name="common_spaces">
|
||||
<item quantity="other">"%1$d Không gian"</item>
|
||||
</plurals>
|
||||
<string name="common_starting_chat">"Đang bắt đầu cuộc trò chuyện…"</string>
|
||||
<string name="common_sticker">"Sticker"</string>
|
||||
<string name="common_success">"Thành công"</string>
|
||||
|
|
@ -337,7 +368,9 @@ Lý do: %1$s ."</string>
|
|||
<string name="common_topic">"Chủ đề"</string>
|
||||
<string name="common_topic_placeholder">"Phòng này dùng để làm gì?"</string>
|
||||
<string name="common_unable_to_decrypt">"Không thể giải mã"</string>
|
||||
<string name="common_unable_to_decrypt_insecure_device">"Được gửi từ một thiết bị không an toàn"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"Bạn không thể xem tin nhắn này"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"Danh tính kỹ thuật số đã được xác minh của người gửi đã được đặt lại."</string>
|
||||
<string name="common_unable_to_invite_message">"Không thể gửi lời mời đến một hoặc nhiều người dùng."</string>
|
||||
<string name="common_unable_to_invite_title">"Không thể gửi lời mời"</string>
|
||||
<string name="common_unlock">"Mở khóa"</string>
|
||||
|
|
@ -383,6 +416,7 @@ Bạn có chắc muốn tiếp tục không?"</string>
|
|||
<string name="dialog_file_too_large_to_upload_subtitle">"Kích thước tệp tối đa cho phép là: %1$s"</string>
|
||||
<string name="dialog_file_too_large_to_upload_title">"Kích thước tệp quá lớn để tải lên"</string>
|
||||
<string name="dialog_room_reported">"Phòng đã được báo cáo"</string>
|
||||
<string name="dialog_room_reported_and_left">"Đã báo cáo và rời khỏi phòng."</string>
|
||||
<string name="dialog_title_confirmation">"Xác nhận"</string>
|
||||
<string name="dialog_title_error">"Lỗi"</string>
|
||||
<string name="dialog_title_success">"Thành công"</string>
|
||||
|
|
@ -393,14 +427,23 @@ Bạn có chắc muốn tiếp tục không?"</string>
|
|||
<string name="dialog_video_quality_selector_subtitle_file_size">"Kích thước tệp tối đa cho phép là: %1$s"</string>
|
||||
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Chọn chất lượng video bạn muốn tải lên."</string>
|
||||
<string name="dialog_video_quality_selector_title">"Chọn chất lượng tải lên video"</string>
|
||||
<string name="emoji_picker_search_placeholder">"Tìm kiếm biểu tượng cảm xúc"</string>
|
||||
<string name="error_account_already_logged_in">"Bạn đã đăng nhập trên thiết bị này với tư cách là%1$s ."</string>
|
||||
<string name="error_account_creation_not_possible">"Máy chủ của bạn cần được nâng cấp để hỗ trợ Dịch vụ Xác thực và tạo tài khoản."</string>
|
||||
<string name="error_failed_creating_the_permalink">"Không tạo được liên kết cố định"</string>
|
||||
<string name="error_failed_loading_map">"%1$s không thể tải bản đồ. Vui lòng thử lại sau."</string>
|
||||
<string name="error_failed_loading_messages">"Không tải được tin nhắn"</string>
|
||||
<string name="error_failed_locating_user">"%1$s không thể truy cập vị trí của bạn. Vui lòng thử lại sau."</string>
|
||||
<string name="error_failed_uploading_voice_message">"Không thể tải lên tin nhắn thoại của bạn."</string>
|
||||
<string name="error_invalid_invite">"Phòng đó không còn tồn tại hoặc lời mời không còn hiệu lực."</string>
|
||||
<string name="error_location_service_disabled_android">"Vui lòng bật GPS để truy cập các tính năng dựa trên vị trí."</string>
|
||||
<string name="error_message_not_found">"Không tìm thấy tin nhắn"</string>
|
||||
<string name="error_missing_location_auth_android">"%1$s không có quyền truy cập vị trí của bạn. Bạn có thể bật quyền trong Cài đặt."</string>
|
||||
<string name="error_missing_location_rationale_android">"%1$s chưa được phép truy cập vị trí. Bật quyền dưới đây."</string>
|
||||
<string name="error_missing_microphone_voice_rationale_android">"%1$s không có quyền truy cập micro của bạn. Hãy bật quyền để ghi tin nhắn thoại."</string>
|
||||
<string name="error_network_or_server_issue">"Nguyên nhân có thể là do sự cố mạng hoặc máy chủ."</string>
|
||||
<string name="error_room_address_already_exists">"Địa chỉ phòng này đã tồn tại. Vui lòng thử chỉnh sửa trường địa chỉ phòng hoặc thay đổi tên phòng."</string>
|
||||
<string name="error_room_address_invalid_symbols">"Một số ký tự không được phép. Chỉ các chữ cái, chữ số và các ký hiệu sau được hỗ trợ: ! $ &amp; ' ( ) * + / ; = ? @ [ ] - . _"</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Một số tin nhắn chưa được gửi"</string>
|
||||
<string name="error_unknown">"Rất tiếc, đã có lỗi xảy ra."</string>
|
||||
<string name="invite_friends_rich_title">"🔐️ Tham gia cùng tôi trên %1$s"</string>
|
||||
|
|
@ -408,6 +451,9 @@ Bạn có chắc muốn tiếp tục không?"</string>
|
|||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Lắc điện thoại để báo cáo lỗi"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Không thể chọn tệp phương tiện. Vui lòng thử lại."</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
<item quantity="other">"%1$d tin nhắn được ghim"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Tin nhắn được ghim"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Xử lý phương tiện tải lên không thành công, vui lòng thử lại."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Không thể lấy thông tin người dùng"</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a11y_add_reaction">"新增反應:%1$s"</string>
|
||||
<string name="a11y_address">"地址"</string>
|
||||
<string name="a11y_avatar">"大頭貼"</string>
|
||||
<string name="a11y_collapse_message_text_field">"最小化訊息文字欄位"</string>
|
||||
<string name="a11y_delete">"刪除"</string>
|
||||
|
|
@ -25,9 +26,12 @@
|
|||
<string name="a11y_pause">"暫停"</string>
|
||||
<string name="a11y_paused_voice_message">"語音訊息,時長:%1$s,目前位置:%2$s"</string>
|
||||
<string name="a11y_pin_field">"PIN 碼欄位"</string>
|
||||
<string name="a11y_pinned_location">"固定位置"</string>
|
||||
<string name="a11y_play">"播放"</string>
|
||||
<string name="a11y_playback_speed">"播放速度"</string>
|
||||
<string name="a11y_poll">"投票"</string>
|
||||
<string name="a11y_poll_end">"投票已結束"</string>
|
||||
<string name="a11y_qr_code">"QR Code"</string>
|
||||
<string name="a11y_react_with">"使用 %1$s 回應"</string>
|
||||
<string name="a11y_react_with_other_emojis">"用其他表情符號回應"</string>
|
||||
<string name="a11y_read_receipts_multiple">"%1$s 和 %2$s 已讀"</string>
|
||||
|
|
@ -40,9 +44,12 @@
|
|||
<string name="a11y_remove_reaction_with">"移除反應 %1$s"</string>
|
||||
<string name="a11y_room_avatar">"聊天室大頭照"</string>
|
||||
<string name="a11y_send_files">"傳送檔案"</string>
|
||||
<string name="a11y_sender_location">"傳送者位置"</string>
|
||||
<string name="a11y_session_verification_time_limited_action_required">"需要限時動作,您有一分鐘可以驗證"</string>
|
||||
<string name="a11y_show_password">"顯示密碼"</string>
|
||||
<string name="a11y_start_call">"開始通話"</string>
|
||||
<string name="a11y_start_video_call">"開始視訊通話"</string>
|
||||
<string name="a11y_start_voice_call">"開始語音通話"</string>
|
||||
<string name="a11y_tombstoned_room">"墓碑聊天室"</string>
|
||||
<string name="a11y_user_avatar">"使用者大頭照"</string>
|
||||
<string name="a11y_user_menu">"使用者選單"</string>
|
||||
|
|
@ -54,6 +61,7 @@
|
|||
<string name="a11y_your_avatar">"您的大頭照"</string>
|
||||
<string name="action_accept">"接受"</string>
|
||||
<string name="action_add_caption">"新增標題"</string>
|
||||
<string name="action_add_existing_rooms">"新增既有聊天室"</string>
|
||||
<string name="action_add_to_timeline">"新增至時間軸"</string>
|
||||
<string name="action_back">"返回"</string>
|
||||
<string name="action_call">"通話"</string>
|
||||
|
|
@ -73,6 +81,7 @@
|
|||
<string name="action_copy_text">"複製文字"</string>
|
||||
<string name="action_create">"建立"</string>
|
||||
<string name="action_create_room">"建立聊天室"</string>
|
||||
<string name="action_create_space">"建立空間"</string>
|
||||
<string name="action_deactivate">"停用"</string>
|
||||
<string name="action_deactivate_account">"停用帳號"</string>
|
||||
<string name="action_decline">"拒絕"</string>
|
||||
|
|
@ -89,6 +98,7 @@
|
|||
<string name="action_enable">"啟用"</string>
|
||||
<string name="action_end_poll">"結束投票"</string>
|
||||
<string name="action_enter_pin">"輸入 PIN 碼"</string>
|
||||
<string name="action_explore_public_spaces">"探索公開空間"</string>
|
||||
<string name="action_finish">"結束"</string>
|
||||
<string name="action_forgot_password">"忘記密碼?"</string>
|
||||
<string name="action_forward">"轉寄"</string>
|
||||
|
|
@ -109,6 +119,7 @@
|
|||
<string name="action_leave_space">"離開空間"</string>
|
||||
<string name="action_load_more">"載入更多"</string>
|
||||
<string name="action_manage_account">"管理帳號"</string>
|
||||
<string name="action_manage_account_and_devices">"管理帳號與裝置"</string>
|
||||
<string name="action_manage_devices">"管理裝置"</string>
|
||||
<string name="action_manage_rooms">"管理聊天室"</string>
|
||||
<string name="action_message">"聊天"</string>
|
||||
|
|
@ -148,16 +159,18 @@
|
|||
<string name="action_send_voice_message">"傳送語音訊息"</string>
|
||||
<string name="action_share">"分享"</string>
|
||||
<string name="action_share_link">"分享連結"</string>
|
||||
<string name="action_share_live_location">"分享即時位置"</string>
|
||||
<string name="action_show">"顯示"</string>
|
||||
<string name="action_sign_in_again">"再登入一次"</string>
|
||||
<string name="action_signout">"登出"</string>
|
||||
<string name="action_signout_anyway">"直接登出"</string>
|
||||
<string name="action_signout">"移除此裝置"</string>
|
||||
<string name="action_signout_anyway">"仍要移除此裝置"</string>
|
||||
<string name="action_skip">"略過"</string>
|
||||
<string name="action_start">"開始"</string>
|
||||
<string name="action_start_chat">"開始聊天"</string>
|
||||
<string name="action_start_over">"重新開始"</string>
|
||||
<string name="action_start_verification">"開始驗證"</string>
|
||||
<string name="action_static_map_load">"點擊以載入地圖"</string>
|
||||
<string name="action_stop">"停止"</string>
|
||||
<string name="action_take_photo">"拍照"</string>
|
||||
<string name="action_tap_for_options">"點擊以查看選項"</string>
|
||||
<string name="action_translate">"翻譯"</string>
|
||||
|
|
@ -178,6 +191,7 @@
|
|||
<string name="common_advanced_settings">"進階設定"</string>
|
||||
<string name="common_an_image">"影像"</string>
|
||||
<string name="common_analytics">"分析"</string>
|
||||
<string name="common_android_fetching_notifications_title">"正在同步通知……"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_left_room">"您離開聊天室"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_session_logged_out">"您已登出工作階段"</string>
|
||||
<string name="common_appearance">"外觀"</string>
|
||||
|
|
@ -190,6 +204,7 @@
|
|||
<string name="common_copied_to_clipboard">"已複製到剪貼簿"</string>
|
||||
<string name="common_copyright">"著作權"</string>
|
||||
<string name="common_creating_room">"正在建立聊天室…"</string>
|
||||
<string name="common_creating_space">"正在建立空間……"</string>
|
||||
<string name="common_current_user_canceled_knock">"請求已取消"</string>
|
||||
<string name="common_current_user_left_room">"已離開聊天室"</string>
|
||||
<string name="common_current_user_left_space">"離開空間"</string>
|
||||
|
|
@ -210,6 +225,7 @@
|
|||
<string name="common_empty_file">"空檔案"</string>
|
||||
<string name="common_encryption">"加密"</string>
|
||||
<string name="common_encryption_enabled">"已啟用加密"</string>
|
||||
<string name="common_ends_at">"結束於 %1$s"</string>
|
||||
<string name="common_enter_your_pin">"輸入您的 PIN 碼"</string>
|
||||
<string name="common_error">"錯誤"</string>
|
||||
<string name="common_error_registering_pusher_android">"發生錯誤,您可能無法收到新訊息的通知。請從設定中進行通知疑難排解。
|
||||
|
|
@ -235,6 +251,9 @@
|
|||
<string name="common_light">"淺色"</string>
|
||||
<string name="common_line_copied_to_clipboard">"行已複製到剪貼簿"</string>
|
||||
<string name="common_link_copied_to_clipboard">"連結已複製到剪貼簿"</string>
|
||||
<string name="common_link_new_device">"連結新裝置"</string>
|
||||
<string name="common_live_location">"即時位置"</string>
|
||||
<string name="common_live_location_ended">"即時位置已結束"</string>
|
||||
<string name="common_loading">"載入中…"</string>
|
||||
<string name="common_loading_more">"載入更多……"</string>
|
||||
<plurals name="common_many_members">
|
||||
|
|
@ -245,10 +264,12 @@
|
|||
</plurals>
|
||||
<string name="common_message">"訊息"</string>
|
||||
<string name="common_message_actions">"訊息動作"</string>
|
||||
<string name="common_message_failed_to_send">"訊息傳送失敗"</string>
|
||||
<string name="common_message_layout">"訊息佈局"</string>
|
||||
<string name="common_message_removed">"訊息已移除"</string>
|
||||
<string name="common_modern">"現代"</string>
|
||||
<string name="common_mute">"關閉通知"</string>
|
||||
<string name="common_name">"名稱"</string>
|
||||
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
||||
<string name="common_no_results">"查無結果"</string>
|
||||
<string name="common_no_room_name">"無聊天室名稱"</string>
|
||||
|
|
@ -257,6 +278,7 @@
|
|||
<string name="common_offline">"離線"</string>
|
||||
<string name="common_open_source_licenses">"開放原始碼授權條款"</string>
|
||||
<string name="common_or">"或"</string>
|
||||
<string name="common_other_options">"其他選項"</string>
|
||||
<string name="common_password">"密碼"</string>
|
||||
<string name="common_people">"夥伴"</string>
|
||||
<string name="common_permalink">"永久連結"</string>
|
||||
|
|
@ -273,8 +295,10 @@
|
|||
</plurals>
|
||||
<string name="common_preparing">"正在準備……"</string>
|
||||
<string name="common_privacy_policy">"隱私權政策"</string>
|
||||
<string name="common_private">"私人"</string>
|
||||
<string name="common_private_room">"私密聊天室"</string>
|
||||
<string name="common_private_space">"私人空間"</string>
|
||||
<string name="common_public">"公開"</string>
|
||||
<string name="common_public_room">"公開的聊天室"</string>
|
||||
<string name="common_public_space">"公開空間"</string>
|
||||
<string name="common_reaction">"回應"</string>
|
||||
|
|
@ -282,6 +306,7 @@
|
|||
<string name="common_reason">"理由"</string>
|
||||
<string name="common_recovery_key">"復原金鑰"</string>
|
||||
<string name="common_refreshing">"重新整理中…"</string>
|
||||
<string name="common_removing">"正在移除……"</string>
|
||||
<plurals name="common_replies">
|
||||
<item quantity="other">"%1$d 個回覆"</item>
|
||||
</plurals>
|
||||
|
|
@ -290,9 +315,10 @@
|
|||
<string name="common_report_a_problem">"回報問題"</string>
|
||||
<string name="common_report_submitted">"已遞交報告"</string>
|
||||
<string name="common_rich_text_editor">"格式化文字編輯器"</string>
|
||||
<string name="common_role">"角色"</string>
|
||||
<string name="common_room">"聊天室"</string>
|
||||
<string name="common_room_name">"聊天室名稱"</string>
|
||||
<string name="common_room_name_placeholder">"範例:您的計畫名稱"</string>
|
||||
<string name="common_room_name_placeholder">"範例:您的專案名稱"</string>
|
||||
<plurals name="common_rooms">
|
||||
<item quantity="other">"%1$d 個聊天室"</item>
|
||||
</plurals>
|
||||
|
|
@ -304,6 +330,9 @@
|
|||
<string name="common_security">"安全性"</string>
|
||||
<string name="common_seen_by">"已讀"</string>
|
||||
<string name="common_select_account">"選取帳號"</string>
|
||||
<plurals name="common_selected_count">
|
||||
<item quantity="other">"已選取 %1$d 個"</item>
|
||||
</plurals>
|
||||
<string name="common_send_to">"傳送給"</string>
|
||||
<string name="common_sending">"傳送中…"</string>
|
||||
<string name="common_sending_failed">"傳送失敗"</string>
|
||||
|
|
@ -314,30 +343,36 @@
|
|||
<string name="common_server_url">"伺服器 URL"</string>
|
||||
<string name="common_settings">"設定"</string>
|
||||
<string name="common_share_space">"分享空間"</string>
|
||||
<string name="common_shared_history">"新成員可以檢視歷史"</string>
|
||||
<string name="common_shared_live_location">"分享即時位置"</string>
|
||||
<string name="common_shared_location">"位置分享"</string>
|
||||
<string name="common_shared_space">"共享空間"</string>
|
||||
<string name="common_signing_out">"正在登出"</string>
|
||||
<string name="common_signing_out">"正在移除裝置"</string>
|
||||
<string name="common_something_went_wrong">"有錯誤發生"</string>
|
||||
<string name="common_something_went_wrong_message">"我們了遇到了問題。請再試一次。"</string>
|
||||
<string name="common_space">"空間"</string>
|
||||
<string name="common_space_members">"空間成員"</string>
|
||||
<string name="common_space_topic_placeholder">"此空間的用途是?"</string>
|
||||
<plurals name="common_spaces">
|
||||
<item quantity="other">"%1$d 個空間"</item>
|
||||
</plurals>
|
||||
<string name="common_starting_chat">"開始聊天…"</string>
|
||||
<string name="common_sticker">"貼圖"</string>
|
||||
<string name="common_success">"成功"</string>
|
||||
<string name="common_suggested">"已建議"</string>
|
||||
<string name="common_suggestions">"建議"</string>
|
||||
<string name="common_syncing">"同步中"</string>
|
||||
<string name="common_system">"系統"</string>
|
||||
<string name="common_text">"文字"</string>
|
||||
<string name="common_third_party_notices">"第三方通知"</string>
|
||||
<string name="common_thread">"討論串"</string>
|
||||
<string name="common_threads">"討論串"</string>
|
||||
<string name="common_topic">"主題"</string>
|
||||
<string name="common_topic_placeholder">"這個聊天室是做什麼用的?"</string>
|
||||
<string name="common_topic_placeholder">"此聊天室的用途是?"</string>
|
||||
<string name="common_unable_to_decrypt">"無法解密"</string>
|
||||
<string name="common_unable_to_decrypt_insecure_device">"從不安全的裝置傳送"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"您無法存取此則訊息"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"傳送者的驗證身份已重設"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"傳送者的驗證數位身份已重設"</string>
|
||||
<string name="common_unable_to_invite_message">"無法發送邀請給一或多個使用者。"</string>
|
||||
<string name="common_unable_to_invite_title">"無法發送邀請"</string>
|
||||
<string name="common_unlock">"解鎖"</string>
|
||||
|
|
@ -362,13 +397,19 @@
|
|||
<string name="common_voice_message">"語音訊息"</string>
|
||||
<string name="common_waiting">"等待中…"</string>
|
||||
<string name="common_waiting_for_decryption_key">"等待此則訊息"</string>
|
||||
<string name="common_waiting_live_location">"正在等待即時位置……"</string>
|
||||
<string name="common_world_readable_history">"任何人都可以檢視歷史"</string>
|
||||
<string name="common_you">"您"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"%1$s 的身份似乎已重設。%2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"%1$s 的 %2$s 身份似乎已重設。%3$s"</string>
|
||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"因為您當時不在聊天室內,所以 %1$s (%2$s) 分享了此訊息。"</string>
|
||||
<string name="crypto_event_key_forwarded_unknown_profile_dialog_content">"因為您當時不在聊天室裡面,因此 %1$s 分享了此訊息。"</string>
|
||||
<string name="crypto_history_visible">"此聊天室被設定為方便新成員閱讀歷史紀錄。%1$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"%1$s 的數位身份似乎已重設。%2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"%1$s 的 %2$s 數位身份似乎已重設。%3$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
|
||||
<string name="crypto_identity_change_profile_pin_violation">"%1$s 的已驗證身份被重設。"</string>
|
||||
<string name="crypto_identity_change_verification_violation_new">"%1$s 的 %2$s 驗證身份已重設。 %3$s"</string>
|
||||
<string name="crypto_identity_change_profile_pin_violation">"%1$s 的數位身份被重設。"</string>
|
||||
<string name="crypto_identity_change_verification_violation_new">"%1$s 的 %2$s 數位身份已重設。%3$s"</string>
|
||||
<string name="crypto_identity_change_withdraw_verification_action">"撤回驗證"</string>
|
||||
<string name="dialog_allow_access">"允許存取"</string>
|
||||
<string name="dialog_confirm_link_message">"連結 %1$s 會將您帶往其他網站 %2$s
|
||||
|
||||
您確定您想要繼續嗎?"</string>
|
||||
|
|
@ -398,6 +439,7 @@
|
|||
<string name="error_failed_locating_user">"%1$s 無法取得您的位置。請稍後再試。"</string>
|
||||
<string name="error_failed_uploading_voice_message">"無法上傳語音訊息。"</string>
|
||||
<string name="error_invalid_invite">"此聊天室不再存在或邀請不再有效。"</string>
|
||||
<string name="error_location_service_disabled_android">"請啟用您的 GPS 以存取以位置為基礎的功能。"</string>
|
||||
<string name="error_message_not_found">"找不到訊息"</string>
|
||||
<string name="error_missing_location_auth_android">"%1$s 沒有權限存取您的位置。您可以到設定中開啟權限。"</string>
|
||||
<string name="error_missing_location_rationale_android">"%1$s 沒有權限存取您的位置。請在下方開啟權限。"</string>
|
||||
|
|
@ -423,11 +465,11 @@
|
|||
<item quantity="other">"%1$d 則釘選的訊息"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"釘選訊息"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"您將要前往您的 %1$s 帳號重設身份。然後您將會被帶回應用程式。"</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"無法確認?前往您的帳號以重設您的身份。"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"您將要前往您的 %1$s 帳號重設數位身份。然後您將會被帶回應用程式。"</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"無法確認?前往您的帳號以重設您的數位身份。"</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_primary_button_title">"撤回驗證並傳送"</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_subtitle">"您可以撤回您的驗證並仍傳送此訊息,或者您也可以立刻取消並在重新驗證 %1$s 後再試一次。"</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_title">"因為 %1$s 的驗證身份已重設,因此未傳送您的訊息。"</string>
|
||||
<string name="screen_resolve_send_failure_changed_identity_title">"因為 %1$s 的驗證數位身份已重設,因此未傳送您的訊息。"</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"仍要傳送訊息"</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_subtitle">"%1$s 正在使用一個或多個未經驗證的裝置。您仍然可以傳送訊息,也可以立刻取消並在 %2$s 驗證其所有裝置後再試一次。"</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_title">"未傳送您的訊息,因為 %1$s 尚未驗證所有裝置。"</string>
|
||||
|
|
@ -439,6 +481,7 @@
|
|||
<string name="screen_room_event_pill">"%1$s 中的訊息"</string>
|
||||
<string name="screen_room_grouped_state_events_expand">"展開"</string>
|
||||
<string name="screen_room_grouped_state_events_reduce">"減少"</string>
|
||||
<string name="screen_room_live_location_banner">"分享即時位置"</string>
|
||||
<string name="screen_room_permalink_same_room_android">"已檢視此聊天室!"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"第 %1$s 個,共 %2$s 個"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s 個釘選訊息"</string>
|
||||
|
|
@ -450,12 +493,16 @@
|
|||
<string name="screen_share_open_apple_maps">"在 Apple Maps 中開啟"</string>
|
||||
<string name="screen_share_open_google_maps">"在 Google Maps 中開啟"</string>
|
||||
<string name="screen_share_open_osm_maps">"在開放街圖(OpenStreetMap) 中開啟"</string>
|
||||
<string name="screen_share_this_location_action">"分享這個位置"</string>
|
||||
<string name="screen_share_this_location_action">"分享選定的位置"</string>
|
||||
<string name="screen_sharing_location_option_sheet_title">"分享選項"</string>
|
||||
<string name="screen_space_list_description">"您建立或加入的空間"</string>
|
||||
<string name="screen_space_list_details">"%1$s • %2$s"</string>
|
||||
<string name="screen_space_list_empty_state_title">"建立空間以整理聊天室"</string>
|
||||
<string name="screen_space_list_parent_space">"%1$s 空間"</string>
|
||||
<string name="screen_space_list_title">"空間"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"因為 %1$s 的驗證身份已重設,因此未傳送訊息。"</string>
|
||||
<string name="screen_static_location_sheet_timestamp_description">"已分享 %1$s"</string>
|
||||
<string name="screen_static_location_sheet_title">"在地圖上"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"因為 %1$s 的驗證數位身份已重設,因此未傳送訊息。"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"訊息未傳送,因為 %1$s 尚未驗證所有裝置。"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"因為您尚未驗證一個或多個裝置,因此未傳送訊息"</string>
|
||||
<string name="screen_view_location_title">"位置"</string>
|
||||
|
|
@ -465,5 +512,5 @@
|
|||
<string name="timeline_decryption_failure_historical_event_unverified_device">"您必須驗證此裝置才能存取歷史訊息"</string>
|
||||
<string name="timeline_decryption_failure_historical_event_user_not_joined">"您無法存取此則訊息"</string>
|
||||
<string name="timeline_decryption_failure_unable_to_decrypt">"無法解密訊息"</string>
|
||||
<string name="timeline_decryption_failure_withheld_unverified">"此訊息被封鎖是因為您沒有驗證您的裝置,或是因為傳送者需要驗證您的身份而被封鎖。"</string>
|
||||
<string name="timeline_decryption_failure_withheld_unverified">"此訊息被封鎖,原因可能是您尚未驗證裝置,或是寄件者需要驗證您的數位身分。"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
<string name="a11y_session_verification_time_limited_action_required">"限时操作,您有一分钟的时间来验证"</string>
|
||||
<string name="a11y_show_password">"显示密码"</string>
|
||||
<string name="a11y_start_call">"开始通话"</string>
|
||||
<string name="a11y_start_video_call">"开始视频通话"</string>
|
||||
<string name="a11y_start_voice_call">"发起语音通话"</string>
|
||||
<string name="a11y_tombstoned_room">"已封存的聊天室"</string>
|
||||
<string name="a11y_user_avatar">"用户头像"</string>
|
||||
|
|
@ -118,6 +119,7 @@
|
|||
<string name="action_leave_space">"离开空间"</string>
|
||||
<string name="action_load_more">"载入更多"</string>
|
||||
<string name="action_manage_account">"管理账户"</string>
|
||||
<string name="action_manage_account_and_devices">"管理账户与设备"</string>
|
||||
<string name="action_manage_devices">"管理设备"</string>
|
||||
<string name="action_manage_rooms">"管理聊天室"</string>
|
||||
<string name="action_message">"发送消息给"</string>
|
||||
|
|
@ -168,6 +170,7 @@
|
|||
<string name="action_start_over">"重新开始"</string>
|
||||
<string name="action_start_verification">"开始验证"</string>
|
||||
<string name="action_static_map_load">"点击以加载地图"</string>
|
||||
<string name="action_stop">"停止"</string>
|
||||
<string name="action_take_photo">"拍摄照片"</string>
|
||||
<string name="action_tap_for_options">"点按查看选项"</string>
|
||||
<string name="action_translate">"翻译"</string>
|
||||
|
|
@ -188,6 +191,7 @@
|
|||
<string name="common_advanced_settings">"高级设置"</string>
|
||||
<string name="common_an_image">"一张图片"</string>
|
||||
<string name="common_analytics">"分析"</string>
|
||||
<string name="common_android_fetching_notifications_title">"正在同步通知…"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_left_room">"你离开了聊天室"</string>
|
||||
<string name="common_android_shortcuts_remove_reason_session_logged_out">"您已退出会话"</string>
|
||||
<string name="common_appearance">"外观"</string>
|
||||
|
|
@ -247,6 +251,8 @@
|
|||
<string name="common_line_copied_to_clipboard">"链接已复制到剪贴板"</string>
|
||||
<string name="common_link_copied_to_clipboard">"链接已复制到剪贴板"</string>
|
||||
<string name="common_link_new_device">"关联新设备"</string>
|
||||
<string name="common_live_location">"实时位置"</string>
|
||||
<string name="common_live_location_ended">"实时位置已结束"</string>
|
||||
<string name="common_loading">"正在加载…"</string>
|
||||
<string name="common_loading_more">"正在加载更多……"</string>
|
||||
<plurals name="common_many_members">
|
||||
|
|
@ -337,9 +343,10 @@
|
|||
<string name="common_settings">"设置"</string>
|
||||
<string name="common_share_space">"共享空间"</string>
|
||||
<string name="common_shared_history">"新成员可见历史记录"</string>
|
||||
<string name="common_shared_live_location">"共享实时位置"</string>
|
||||
<string name="common_shared_location">"共享位置"</string>
|
||||
<string name="common_shared_space">"共享空间"</string>
|
||||
<string name="common_signing_out">"正在登出"</string>
|
||||
<string name="common_signing_out">"正在移除设备"</string>
|
||||
<string name="common_something_went_wrong">"发生了一些错误"</string>
|
||||
<string name="common_something_went_wrong_message">"我们遇到了一个问题。请重试。"</string>
|
||||
<string name="common_space">"空间"</string>
|
||||
|
|
@ -358,6 +365,7 @@
|
|||
<string name="common_text">"文本"</string>
|
||||
<string name="common_third_party_notices">"第三方通知"</string>
|
||||
<string name="common_thread">"消息列"</string>
|
||||
<string name="common_threads">"消息列"</string>
|
||||
<string name="common_topic">"主题"</string>
|
||||
<string name="common_topic_placeholder">"该聊天室的主题是什么?"</string>
|
||||
<string name="common_unable_to_decrypt">"无法解密"</string>
|
||||
|
|
@ -388,6 +396,7 @@
|
|||
<string name="common_voice_message">"语音消息"</string>
|
||||
<string name="common_waiting">"等待…"</string>
|
||||
<string name="common_waiting_for_decryption_key">"正在等待解密密钥"</string>
|
||||
<string name="common_waiting_live_location">"正在等待实时位置…"</string>
|
||||
<string name="common_world_readable_history">"任何人都可查看历史记录"</string>
|
||||
<string name="common_you">"您"</string>
|
||||
<string name="crypto_event_key_forwarded_known_profile_dialog_content">"%1$s (%2$s) 由于您当时不在聊天室内,系统已将消息共享给您。"</string>
|
||||
|
|
@ -448,8 +457,10 @@
|
|||
<string name="screen_create_poll_options_section_title">"选项"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"移除%1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"设置"</string>
|
||||
<string name="screen_live_location_sheet_nobody_sharing">"目前无人分享其位置"</string>
|
||||
<string name="screen_live_location_sheet_sharing_live_location">"共享实时位置"</string>
|
||||
<string name="screen_live_location_sheet_title">"在地图上"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"选择媒体失败,请重试。"</string>
|
||||
<string name="screen_onboarding_welcome_back">"欢迎回来"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"按下消息并选择 “%1$s” 将其包含在此处。"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"固定重要消息,以便轻松发现它们"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@
|
|||
<string name="common_about">"About"</string>
|
||||
<string name="common_acceptable_use_policy">"Acceptable use policy"</string>
|
||||
<string name="common_add_account">"Add an account"</string>
|
||||
<string name="common_add_another_account">"Add another account"</string>
|
||||
<string name="common_add_another_account">"Add account"</string>
|
||||
<string name="common_adding_caption">"Adding caption"</string>
|
||||
<string name="common_advanced_settings">"Advanced settings"</string>
|
||||
<string name="common_an_image">"an image"</string>
|
||||
|
|
@ -467,16 +467,14 @@ Are you sure you want to continue?"</string>
|
|||
<string name="screen_create_poll_options_section_title">"Options"</string>
|
||||
<string name="screen_create_poll_remove_accessibility_label">"Remove %1$s"</string>
|
||||
<string name="screen_create_poll_settings_section_title">"Settings"</string>
|
||||
<string name="screen_live_location_sheet_nobody_sharing">"Nobody is sharing their location"</string>
|
||||
<string name="screen_live_location_sheet_sharing_live_location">"Sharing live location"</string>
|
||||
<plurals name="screen_live_location_sheet_subtitle">
|
||||
<item quantity="one">"%1$d person"</item>
|
||||
<item quantity="other">"%1$d people"</item>
|
||||
</plurals>
|
||||
<string name="screen_live_location_sheet_title">"On the map"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_missing_key_backup_open_element_classic">"Open Element Classic"</string>
|
||||
<string name="screen_missing_key_backup_step_1">"Open Element Classic on your device"</string>
|
||||
<string name="screen_missing_key_backup_step_2_android">"Go to Settings > Security & Privacy"</string>
|
||||
<string name="screen_missing_key_backup_step_3_android">"In Cryptography keys management, select Encrypted messages recovery"</string>
|
||||
<string name="screen_missing_key_backup_step_4">"Follow the instructions to enable your key storage"</string>
|
||||
<string name="screen_missing_key_backup_step_5">"Come back to %1$s"</string>
|
||||
<string name="screen_missing_key_backup_title">"Enable your key storage before proceeding to %1$s"</string>
|
||||
<string name="screen_onboarding_checking_account">"Checking account"</string>
|
||||
<string name="screen_onboarding_welcome_back">"Welcome back"</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Press on a message and choose “%1$s” to include here."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Pin important messages so that they can be easily discovered"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
|
|
|
|||
10
libraries/ui-strings/src/main/res/values/temporary.xml
Normal file
10
libraries/ui-strings/src/main/res/values/temporary.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2026 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="common_black">"Black"</string>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue