Merge branch 'develop' into renovate/kotlin
This commit is contained in:
commit
5bc7b897cc
1157 changed files with 7158 additions and 4277 deletions
|
|
@ -11,8 +11,9 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
|
|||
|
||||
const val A_FORMATTED_DATE = "formatted_date"
|
||||
|
||||
class FakeLastMessageTimestampFormatter : LastMessageTimestampFormatter {
|
||||
private var format = ""
|
||||
class FakeLastMessageTimestampFormatter(
|
||||
var format: String = "",
|
||||
) : LastMessageTimestampFormatter {
|
||||
fun givenFormat(format: String) {
|
||||
this.format = format
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.atoms
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.Badge
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.badgeNegativeBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.badgeNegativeContentColor
|
||||
import io.element.android.libraries.designsystem.theme.badgeNeutralBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.badgeNeutralContentColor
|
||||
import io.element.android.libraries.designsystem.theme.badgePositiveBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.badgePositiveContentColor
|
||||
|
||||
object MatrixBadgeAtom {
|
||||
data class MatrixBadgeData(
|
||||
val text: String,
|
||||
val icon: ImageVector,
|
||||
val type: Type,
|
||||
)
|
||||
|
||||
enum class Type {
|
||||
Positive,
|
||||
Neutral,
|
||||
Negative
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun View(
|
||||
data: MatrixBadgeData,
|
||||
) {
|
||||
val backgroundColor = when (data.type) {
|
||||
Type.Positive -> ElementTheme.colors.badgePositiveBackgroundColor
|
||||
Type.Neutral -> ElementTheme.colors.badgeNeutralBackgroundColor
|
||||
Type.Negative -> ElementTheme.colors.badgeNegativeBackgroundColor
|
||||
}
|
||||
val textColor = when (data.type) {
|
||||
Type.Positive -> ElementTheme.colors.badgePositiveContentColor
|
||||
Type.Neutral -> ElementTheme.colors.badgeNeutralContentColor
|
||||
Type.Negative -> ElementTheme.colors.badgeNegativeContentColor
|
||||
}
|
||||
val iconColor = when (data.type) {
|
||||
Type.Positive -> ElementTheme.colors.iconSuccessPrimary
|
||||
Type.Neutral -> ElementTheme.colors.iconSecondary
|
||||
Type.Negative -> ElementTheme.colors.iconCriticalPrimary
|
||||
}
|
||||
Badge(
|
||||
text = data.text,
|
||||
icon = data.icon,
|
||||
backgroundColor = backgroundColor,
|
||||
iconColor = iconColor,
|
||||
textColor = textColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MatrixBadgeAtomPositivePreview() = ElementPreview {
|
||||
MatrixBadgeAtom.View(
|
||||
MatrixBadgeAtom.MatrixBadgeData(
|
||||
text = "Trusted",
|
||||
icon = CompoundIcons.Verified(),
|
||||
type = MatrixBadgeAtom.Type.Positive,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MatrixBadgeAtomNeutralPreview() = ElementPreview {
|
||||
MatrixBadgeAtom.View(
|
||||
MatrixBadgeAtom.MatrixBadgeData(
|
||||
text = "Public room",
|
||||
icon = CompoundIcons.Public(),
|
||||
type = MatrixBadgeAtom.Type.Neutral,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MatrixBadgeAtomNegativePreview() = ElementPreview {
|
||||
MatrixBadgeAtom.View(
|
||||
MatrixBadgeAtom.MatrixBadgeData(
|
||||
text = "Not trusted",
|
||||
icon = CompoundIcons.Error(),
|
||||
type = MatrixBadgeAtom.Type.Negative,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
|||
@Composable
|
||||
fun RoundedIconAtom(
|
||||
modifier: Modifier = Modifier,
|
||||
size: RoundedIconAtomSize = RoundedIconAtomSize.Large,
|
||||
size: RoundedIconAtomSize = RoundedIconAtomSize.Big,
|
||||
resourceId: Int? = null,
|
||||
imageVector: ImageVector? = null,
|
||||
tint: Color = MaterialTheme.colorScheme.secondary,
|
||||
|
|
@ -71,21 +71,21 @@ fun RoundedIconAtom(
|
|||
private fun RoundedIconAtomSize.toContainerSize(): Dp {
|
||||
return when (this) {
|
||||
RoundedIconAtomSize.Medium -> 30.dp
|
||||
RoundedIconAtomSize.Large -> 70.dp
|
||||
RoundedIconAtomSize.Big -> 36.dp
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoundedIconAtomSize.toCornerSize(): Dp {
|
||||
return when (this) {
|
||||
RoundedIconAtomSize.Medium -> 8.dp
|
||||
RoundedIconAtomSize.Large -> 14.dp
|
||||
RoundedIconAtomSize.Big -> 8.dp
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoundedIconAtomSize.toIconSize(): Dp {
|
||||
return when (this) {
|
||||
RoundedIconAtomSize.Medium -> 16.dp
|
||||
RoundedIconAtomSize.Large -> 48.dp
|
||||
RoundedIconAtomSize.Big -> 24.dp
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ internal fun RoundedIconAtomPreview() = ElementPreview {
|
|||
imageVector = Icons.Filled.Home,
|
||||
)
|
||||
RoundedIconAtom(
|
||||
size = RoundedIconAtomSize.Large,
|
||||
size = RoundedIconAtomSize.Big,
|
||||
imageVector = Icons.Filled.Home,
|
||||
)
|
||||
}
|
||||
|
|
@ -106,5 +106,5 @@ internal fun RoundedIconAtomPreview() = ElementPreview {
|
|||
|
||||
enum class RoundedIconAtomSize {
|
||||
Medium,
|
||||
Large
|
||||
Big,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,50 +15,34 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
||||
|
||||
/**
|
||||
* IconTitleSubtitleMolecule is a molecule which displays an icon, a title and a subtitle.
|
||||
*
|
||||
* @param title the title to display
|
||||
* @param subTitle the subtitle to display
|
||||
* @param iconStyle the style of the [BigIcon] to display
|
||||
* @param modifier the modifier to apply to this layout
|
||||
* @param iconResourceId the resource id of the icon to display, exclusive with [iconImageVector]
|
||||
* @param iconImageVector the image vector of the icon to display, exclusive with [iconResourceId]
|
||||
* @param iconTint the tint to apply to the icon
|
||||
* @param iconBackgroundTint the tint to apply to the icon background
|
||||
*/
|
||||
@Composable
|
||||
fun IconTitleSubtitleMolecule(
|
||||
title: String,
|
||||
subTitle: String?,
|
||||
iconStyle: BigIcon.Style,
|
||||
modifier: Modifier = Modifier,
|
||||
iconResourceId: Int? = null,
|
||||
iconImageVector: ImageVector? = null,
|
||||
iconTint: Color = MaterialTheme.colorScheme.primary,
|
||||
iconBackgroundTint: Color = ElementTheme.colors.temporaryColorBgSpecial,
|
||||
) {
|
||||
Column(modifier) {
|
||||
RoundedIconAtom(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally),
|
||||
size = RoundedIconAtomSize.Large,
|
||||
resourceId = iconResourceId,
|
||||
imageVector = iconImageVector,
|
||||
tint = iconTint,
|
||||
backgroundTint = iconBackgroundTint,
|
||||
BigIcon(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
style = iconStyle,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
|
|
@ -86,18 +70,7 @@ fun IconTitleSubtitleMolecule(
|
|||
@Composable
|
||||
internal fun IconTitleSubtitleMoleculePreview() = ElementPreview {
|
||||
IconTitleSubtitleMolecule(
|
||||
iconImageVector = CompoundIcons.Chat(),
|
||||
title = "Title",
|
||||
subTitle = "Subtitle",
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun IconTitleSubtitleMoleculeWithResIconPreview() = ElementPreview {
|
||||
IconTitleSubtitleMolecule(
|
||||
iconResourceId = CompoundDrawables.ic_compound_admin,
|
||||
iconTint = Color.Black,
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Chat()),
|
||||
title = "Title",
|
||||
subTitle = "Subtitle",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.molecules
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun MatrixBadgeRowMolecule(
|
||||
data: ImmutableList<MatrixBadgeAtom.MatrixBadgeData>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
for (badge in data) {
|
||||
MatrixBadgeAtom.View(badge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.molecules
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun TextWithLabelMolecule(
|
||||
label: String,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = label,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,12 +30,12 @@ import io.element.android.compound.theme.ElementTheme
|
|||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.bigIconDefaultBackgroundColor
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* Compound component that display a big icon centered in a rounded square.
|
||||
* Figma: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=1960-553&node-type=frame&m=dev
|
||||
*/
|
||||
object BigIcon {
|
||||
/**
|
||||
|
|
@ -84,7 +84,7 @@ object BigIcon {
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backgroundColor = when (style) {
|
||||
is Style.Default -> ElementTheme.colors.bigIconDefaultBackgroundColor
|
||||
is Style.Default -> ElementTheme.colors.bgSubtleSecondary
|
||||
Style.Alert, Style.Success -> Color.Transparent
|
||||
Style.AlertSolid -> ElementTheme.colors.bgCriticalSubtle
|
||||
Style.SuccessSolid -> ElementTheme.colors.bgSuccessSubtle
|
||||
|
|
@ -100,7 +100,7 @@ object BigIcon {
|
|||
Style.Success, Style.SuccessSolid -> stringResource(CommonStrings.common_success)
|
||||
}
|
||||
val iconTint = when (style) {
|
||||
is Style.Default -> ElementTheme.colors.iconSecondaryAlpha
|
||||
is Style.Default -> ElementTheme.colors.iconSecondary
|
||||
Style.Alert, Style.AlertSolid -> ElementTheme.colors.iconCriticalPrimary
|
||||
Style.Success, Style.SuccessSolid -> ElementTheme.colors.iconSuccessPrimary
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,10 +132,6 @@ val SemanticColors.mentionPillBackground
|
|||
Color(0x26f4f7fa)
|
||||
}
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.bigIconDefaultBackgroundColor
|
||||
get() = if (isLight) LightColorTokens.colorAlphaGray300 else DarkColorTokens.colorAlphaGray300
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.bigCheckmarkBorderColor
|
||||
get() = if (isLight) LightColorTokens.colorGray400 else DarkColorTokens.colorGray400
|
||||
|
|
@ -195,7 +191,6 @@ internal fun ColorAliasesPreview() = ElementPreview {
|
|||
"progressIndicatorTrackColor" to ElementTheme.colors.progressIndicatorTrackColor,
|
||||
"temporaryColorBgSpecial" to ElementTheme.colors.temporaryColorBgSpecial,
|
||||
"iconSuccessPrimaryBackground" to ElementTheme.colors.iconSuccessPrimaryBackground,
|
||||
"bigIconBackgroundColor" to ElementTheme.colors.bigIconDefaultBackgroundColor,
|
||||
"bigCheckmarkBorderColor" to ElementTheme.colors.bigCheckmarkBorderColor,
|
||||
"highlightedMessageBackgroundColor" to ElementTheme.colors.highlightedMessageBackgroundColor,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ fun Checkbox(
|
|||
@Composable
|
||||
private fun compoundCheckBoxColors(): CheckboxColors {
|
||||
return CheckboxDefaults.colors(
|
||||
checkedColor = ElementTheme.materialColors.primary,
|
||||
checkedColor = ElementTheme.colors.bgAccentRest,
|
||||
uncheckedColor = ElementTheme.colors.borderInteractivePrimary,
|
||||
checkmarkColor = ElementTheme.materialColors.onPrimary,
|
||||
disabledUncheckedColor = ElementTheme.colors.borderDisabled,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.ListItemColors
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
|
|
@ -41,6 +42,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
|||
* @param trailingContent The content to be displayed after the headline content.
|
||||
* @param style The style to use for the list item. This may change the color and text styles of the contents. [ListItemStyle.Default] is used by default.
|
||||
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
|
||||
* @param alwaysClickable Whether the list item should always be clickable, even when disabled.
|
||||
* @param onClick The callback to be called when the list item is clicked.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -53,6 +55,7 @@ fun ListItem(
|
|||
trailingContent: ListItemContent? = null,
|
||||
style: ListItemStyle = ListItemStyle.Default,
|
||||
enabled: Boolean = true,
|
||||
alwaysClickable: Boolean = false,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val colors = ListItemDefaults.colors(
|
||||
|
|
@ -73,6 +76,7 @@ fun ListItem(
|
|||
trailingContent = trailingContent,
|
||||
colors = colors,
|
||||
enabled = enabled,
|
||||
alwaysClickable = alwaysClickable,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -86,6 +90,7 @@ fun ListItem(
|
|||
* @param leadingContent The content to be displayed before the headline content.
|
||||
* @param trailingContent The content to be displayed after the headline content.
|
||||
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
|
||||
* @param alwaysClickable Whether the list item should always be clickable, even when disabled.
|
||||
* @param onClick The callback to be called when the list item is clicked.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -98,10 +103,12 @@ fun ListItem(
|
|||
leadingContent: ListItemContent? = null,
|
||||
trailingContent: ListItemContent? = null,
|
||||
enabled: Boolean = true,
|
||||
alwaysClickable: Boolean = false,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
// We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
|
||||
val headlineColor = if (enabled) colors.headlineColor else colors.disabledHeadlineColor
|
||||
val supportingColor = if (enabled) colors.supportingTextColor else colors.disabledHeadlineColor.copy(alpha = 0.80f)
|
||||
val leadingContentColor = if (enabled) colors.leadingIconColor else colors.disabledLeadingIconColor
|
||||
val trailingContentColor = if (enabled) colors.trailingIconColor else colors.disabledTrailingIconColor
|
||||
|
||||
|
|
@ -117,6 +124,7 @@ fun ListItem(
|
|||
{
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides ElementTheme.materialTypography.bodyMedium,
|
||||
LocalContentColor provides supportingColor,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
|
@ -146,7 +154,7 @@ fun ListItem(
|
|||
headlineContent = decoratedHeadlineContent,
|
||||
modifier = if (onClick != null) {
|
||||
Modifier
|
||||
.clickable(enabled = enabled, onClick = onClick)
|
||||
.clickable(enabled = enabled || alwaysClickable, onClick = onClick)
|
||||
.then(modifier)
|
||||
} else {
|
||||
modifier
|
||||
|
|
@ -376,33 +384,91 @@ internal fun ListItemPrimaryActionWithIconPreview() = PreviewItems.OneLineListIt
|
|||
// endregion
|
||||
|
||||
// region: Error state
|
||||
@Preview(name = "List item - Error", group = PreviewGroup.ListItems)
|
||||
@Preview(name = "List item (2 lines) - Simple - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemErrorPreview() = PreviewItems.OneLineListItemPreview(style = ListItemStyle.Destructive)
|
||||
|
||||
@Preview(name = "List item - Error & Icon", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemErrorWithIconPreview() = PreviewItems.OneLineListItemPreview(
|
||||
style = ListItemStyle.Destructive,
|
||||
leadingContent = PreviewItems.icon(),
|
||||
internal fun ListItemTwoLinesSimpleErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
style = ListItemStyle.Destructive
|
||||
)
|
||||
// endregion
|
||||
|
||||
// region: Disabled state
|
||||
@Preview(name = "List item - Disabled", group = PreviewGroup.ListItems)
|
||||
@Preview(name = "List item (2 lines) - Trailing Checkbox - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemDisabledPreview() = PreviewItems.OneLineListItemPreview(enabled = false)
|
||||
internal fun ListItemTwoLinesTrailingCheckBoxErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
trailingContent = PreviewItems.checkbox(),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
|
||||
@Preview(name = "List item - Disabled & Icon", group = PreviewGroup.ListItems)
|
||||
@Preview(name = "List item (2 lines) - Trailing RadioButton - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemDisabledWithIconPreview() = PreviewItems.OneLineListItemPreview(
|
||||
enabled = false,
|
||||
internal fun ListItemTwoLinesTrailingRadioButtonErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
trailingContent = PreviewItems.radioButton(),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
|
||||
@Preview(name = "List item (2 lines) - Trailing Switch - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemTwoLinesTrailingSwitchErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
trailingContent = PreviewItems.switch(),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
|
||||
@Preview(name = "List item (2 lines) - Trailing Icon - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemTwoLinesTrailingIconErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
trailingContent = PreviewItems.icon(),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
|
||||
// region: Leading Checkbox
|
||||
@Preview(name = "List item (2 lines) - Leading Checkbox - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemTwoLinesLeadingCheckboxErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
leadingContent = PreviewItems.checkbox(),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
|
||||
@Preview(name = "List item (2 lines) - Leading RadioButton - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemTwoLinesLeadingRadioButtonErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
leadingContent = PreviewItems.radioButton(),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
|
||||
@Preview(name = "List item (2 lines) - Leading Switch - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemTwoLinesLeadingSwitchErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
leadingContent = PreviewItems.switch(),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
|
||||
@Preview(name = "List item (2 lines) - Leading Icon - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemTwoLinesLeadingIconErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
leadingContent = PreviewItems.icon(),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
|
||||
@Preview(name = "List item (2 lines) - Both Icons - Error", group = PreviewGroup.ListItems)
|
||||
@Composable
|
||||
internal fun ListItemTwoLinesBothIconsErrorPreview() = PreviewItems.TwoLinesListItemPreview(
|
||||
leadingContent = PreviewItems.icon(),
|
||||
trailingContent = PreviewItems.icon(),
|
||||
style = ListItemStyle.Destructive,
|
||||
)
|
||||
// endregion
|
||||
|
||||
@Suppress("ModifierMissing")
|
||||
private object PreviewItems {
|
||||
@Composable
|
||||
private fun EnabledDisabledElementThemedPreview(
|
||||
content: @Composable (Boolean) -> Unit,
|
||||
) = ElementThemedPreview {
|
||||
Column {
|
||||
sequenceOf(true, false).forEach {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThreeLinesListItemPreview(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -410,12 +476,13 @@ private object PreviewItems {
|
|||
leadingContent: ListItemContent? = null,
|
||||
trailingContent: ListItemContent? = null,
|
||||
) {
|
||||
ElementThemedPreview {
|
||||
EnabledDisabledElementThemedPreview {
|
||||
ListItem(
|
||||
headlineContent = headline(),
|
||||
supportingContent = text(),
|
||||
leadingContent = leadingContent,
|
||||
trailingContent = trailingContent,
|
||||
enabled = it,
|
||||
style = style,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
@ -429,12 +496,13 @@ private object PreviewItems {
|
|||
leadingContent: ListItemContent? = null,
|
||||
trailingContent: ListItemContent? = null,
|
||||
) {
|
||||
ElementThemedPreview {
|
||||
EnabledDisabledElementThemedPreview {
|
||||
ListItem(
|
||||
headlineContent = headline(),
|
||||
supportingContent = textSingleLine(),
|
||||
leadingContent = leadingContent,
|
||||
trailingContent = trailingContent,
|
||||
enabled = it,
|
||||
style = style,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
@ -447,14 +515,13 @@ private object PreviewItems {
|
|||
style: ListItemStyle = ListItemStyle.Default,
|
||||
leadingContent: ListItemContent? = null,
|
||||
trailingContent: ListItemContent? = null,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
ElementThemedPreview {
|
||||
EnabledDisabledElementThemedPreview {
|
||||
ListItem(
|
||||
headlineContent = headline(),
|
||||
leadingContent = leadingContent,
|
||||
trailingContent = trailingContent,
|
||||
enabled = enabled,
|
||||
enabled = it,
|
||||
style = style,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ fun RadioButton(
|
|||
internal fun compoundRadioButtonColors(): RadioButtonColors {
|
||||
return RadioButtonDefaults.colors(
|
||||
unselectedColor = ElementTheme.colors.borderInteractivePrimary,
|
||||
selectedColor = ElementTheme.colors.bgAccentRest,
|
||||
disabledUnselectedColor = ElementTheme.colors.borderDisabled,
|
||||
disabledSelectedColor = ElementTheme.colors.iconDisabled,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,8 +54,10 @@ fun Switch(
|
|||
|
||||
@Composable
|
||||
internal fun compoundSwitchColors() = SwitchDefaults.colors(
|
||||
uncheckedThumbColor = ElementTheme.colors.bgActionPrimaryRest,
|
||||
uncheckedThumbColor = ElementTheme.colors.iconSecondary,
|
||||
uncheckedBorderColor = ElementTheme.colors.borderInteractivePrimary,
|
||||
uncheckedTrackColor = Color.Transparent,
|
||||
checkedTrackColor = ElementTheme.colors.bgAccentRest,
|
||||
disabledUncheckedBorderColor = ElementTheme.colors.borderDisabled,
|
||||
disabledUncheckedThumbColor = ElementTheme.colors.iconDisabled,
|
||||
disabledCheckedTrackColor = ElementTheme.colors.iconDisabled,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,12 @@
|
|||
<string name="state_event_room_name_removed_by_you">"Anda menghapus nama ruangan"</string>
|
||||
<string name="state_event_room_none">"%1$s tidak membuat perubahan"</string>
|
||||
<string name="state_event_room_none_by_you">"Anda tidak membuat perubahan"</string>
|
||||
<string name="state_event_room_pinned_events_changed">"%1$s mengubah pesan yang disematkan"</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"Anda mengubah pesan yang disematkan"</string>
|
||||
<string name="state_event_room_pinned_events_pinned">"%1$s menyematkan pesan"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"Anda menyematkan pesan"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned">"%1$s melepas sematan pesan"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"Anda melepas sematan pesan"</string>
|
||||
<string name="state_event_room_reject">"%1$s menolak undangan"</string>
|
||||
<string name="state_event_room_reject_by_you">"Anda menolak undangan"</string>
|
||||
<string name="state_event_room_remove">"%1$s mengeluarkan %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
|
|||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||
|
|
@ -52,7 +52,7 @@ interface MatrixClient : Closeable {
|
|||
val sessionCoroutineScope: CoroutineScope
|
||||
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
|
||||
suspend fun getRoom(roomId: RoomId): MatrixRoom?
|
||||
suspend fun getInvitedRoom(roomId: RoomId): InvitedRoom?
|
||||
suspend fun getPendingRoom(roomId: RoomId): PendingRoom?
|
||||
suspend fun findDM(userId: UserId): RoomId?
|
||||
suspend fun ignoreUser(userId: UserId): Result<Unit>
|
||||
suspend fun unignoreUser(userId: UserId): Result<Unit>
|
||||
|
|
@ -65,7 +65,7 @@ interface MatrixClient : Closeable {
|
|||
suspend fun removeAvatar(): Result<Unit>
|
||||
suspend fun joinRoom(roomId: RoomId): Result<RoomSummary?>
|
||||
suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomSummary?>
|
||||
suspend fun knockRoom(roomId: RoomId): Result<Unit>
|
||||
suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?>
|
||||
fun syncService(): SyncService
|
||||
fun sessionVerificationService(): SessionVerificationService
|
||||
fun pushersService(): PushersService
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.core
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
@JvmInline
|
||||
value class FlowId(val value: String) : Serializable {
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
|
@ -60,6 +60,8 @@ interface EncryptionService {
|
|||
*/
|
||||
suspend fun startIdentityReset(): Result<IdentityResetHandle?>
|
||||
|
||||
suspend fun isUserVerified(userId: UserId): Result<Boolean>
|
||||
|
||||
/**
|
||||
* Remember this identity, ensuring it does not result in a pin violation.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ package io.element.android.libraries.matrix.api.room
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
/** A reference to a room the current user has been invited to, with the ability to decline the invite. */
|
||||
interface InvitedRoom : AutoCloseable {
|
||||
/** A reference to a room the current user has knocked to or has been invited to, with the ability to leave the room. */
|
||||
interface PendingRoom : AutoCloseable {
|
||||
val sessionId: SessionId
|
||||
val roomId: RoomId
|
||||
|
||||
/** Decline the invite to this room. */
|
||||
suspend fun declineInvite(): Result<Unit>
|
||||
/** Leave the room ie.decline invite or cancel knock. */
|
||||
suspend fun leave(): Result<Unit>
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ sealed interface LocalEventSendState {
|
|||
data object Sending : LocalEventSendState
|
||||
sealed interface Failed : LocalEventSendState {
|
||||
data class Unknown(val error: String) : Failed
|
||||
data object CrossSigningNotSetup : Failed
|
||||
data object SendingFromUnverifiedDevice : Failed
|
||||
|
||||
sealed interface VerifiedUser : Failed
|
||||
|
|
|
|||
|
|
@ -9,5 +9,8 @@ package io.element.android.libraries.matrix.api.timeline.item.event
|
|||
|
||||
enum class UtdCause {
|
||||
Unknown,
|
||||
Membership,
|
||||
SentBeforeWeJoined,
|
||||
VerificationViolation,
|
||||
UnsignedDevice,
|
||||
UnknownDevice
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.verification
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.FlowId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class SessionVerificationRequestDetails(
|
||||
val senderId: UserId,
|
||||
val flowId: FlowId,
|
||||
val deviceId: DeviceId,
|
||||
val displayName: String?,
|
||||
val firstSeenTimestamp: Long,
|
||||
) : Parcelable
|
||||
|
|
@ -56,7 +56,27 @@ interface SessionVerificationService {
|
|||
/**
|
||||
* Returns the verification service state to the initial step.
|
||||
*/
|
||||
suspend fun reset()
|
||||
suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean)
|
||||
|
||||
/**
|
||||
* Register a listener to be notified of incoming session verification requests.
|
||||
*/
|
||||
fun setListener(listener: SessionVerificationServiceListener?)
|
||||
|
||||
/**
|
||||
* Set this particular request as the currently active one and register for
|
||||
* events pertaining it.
|
||||
*/
|
||||
suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails)
|
||||
|
||||
/**
|
||||
* Accept the previously acknowledged verification request.
|
||||
*/
|
||||
suspend fun acceptVerificationRequest()
|
||||
}
|
||||
|
||||
interface SessionVerificationServiceListener {
|
||||
fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails)
|
||||
}
|
||||
|
||||
/** Verification status of the current session. */
|
||||
|
|
@ -82,20 +102,20 @@ sealed interface VerificationFlowState {
|
|||
data object Initial : VerificationFlowState
|
||||
|
||||
/** Session verification request was accepted by another device. */
|
||||
data object AcceptedVerificationRequest : VerificationFlowState
|
||||
data object DidAcceptVerificationRequest : VerificationFlowState
|
||||
|
||||
/** Short Authentication String (SAS) verification started between the 2 devices. */
|
||||
data object StartedSasVerification : VerificationFlowState
|
||||
data object DidStartSasVerification : VerificationFlowState
|
||||
|
||||
/** Verification data for the SAS verification received. */
|
||||
data class ReceivedVerificationData(val data: SessionVerificationData) : VerificationFlowState
|
||||
data class DidReceiveVerificationData(val data: SessionVerificationData) : VerificationFlowState
|
||||
|
||||
/** Verification completed successfully. */
|
||||
data object Finished : VerificationFlowState
|
||||
data object DidFinish : VerificationFlowState
|
||||
|
||||
/** Verification was cancelled by either device. */
|
||||
data object Canceled : VerificationFlowState
|
||||
data object DidCancel : VerificationFlowState
|
||||
|
||||
/** Verification failed with an error. */
|
||||
data object Failed : VerificationFlowState
|
||||
data object DidFail : VerificationFlowState
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification
|
|||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||
|
|
@ -251,24 +251,26 @@ class RustMatrixClient(
|
|||
return roomFactory.create(roomId)
|
||||
}
|
||||
|
||||
override suspend fun getInvitedRoom(roomId: RoomId): InvitedRoom? {
|
||||
return roomFactory.createInvitedRoom(roomId)
|
||||
override suspend fun getPendingRoom(roomId: RoomId): PendingRoom? {
|
||||
return roomFactory.createPendingRoom(roomId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the room to be available in the room list, with a membership for the current user of [CurrentUserMembership.JOINED].
|
||||
* Wait for the room to be available in the room list with the correct membership for the current user.
|
||||
* @param roomIdOrAlias the room id or alias to wait for
|
||||
* @param timeout the timeout to wait for the room to be available
|
||||
* @param currentUserMembership the membership to wait for
|
||||
* @throws TimeoutCancellationException if the room is not available after the timeout
|
||||
*/
|
||||
private suspend fun awaitJoinedRoom(
|
||||
private suspend fun awaitRoom(
|
||||
roomIdOrAlias: RoomIdOrAlias,
|
||||
timeout: Duration
|
||||
timeout: Duration,
|
||||
currentUserMembership: CurrentUserMembership,
|
||||
): RoomSummary {
|
||||
return withTimeout(timeout) {
|
||||
getRoomSummaryFlow(roomIdOrAlias)
|
||||
.mapNotNull { optionalRoomSummary -> optionalRoomSummary.getOrNull() }
|
||||
.filter { roomSummary -> roomSummary.info.currentUserMembership == CurrentUserMembership.JOINED }
|
||||
.filter { roomSummary -> roomSummary.info.currentUserMembership == currentUserMembership }
|
||||
.first()
|
||||
// Ensure that the room is ready
|
||||
.also { client.awaitRoomRemoteEcho(it.roomId.value) }
|
||||
|
|
@ -314,7 +316,7 @@ class RustMatrixClient(
|
|||
val roomId = RoomId(client.createRoom(rustParams))
|
||||
// Wait to receive the room back from the sync but do not returns failure if it fails.
|
||||
try {
|
||||
awaitJoinedRoom(roomId.toRoomIdOrAlias(), 30.seconds)
|
||||
awaitRoom(roomId.toRoomIdOrAlias(), 30.seconds, CurrentUserMembership.JOINED)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||
}
|
||||
|
|
@ -369,7 +371,7 @@ class RustMatrixClient(
|
|||
runCatching {
|
||||
client.joinRoomById(roomId.value).destroy()
|
||||
try {
|
||||
awaitJoinedRoom(roomId.toRoomIdOrAlias(), 10.seconds)
|
||||
awaitRoom(roomId.toRoomIdOrAlias(), 10.seconds, CurrentUserMembership.JOINED)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||
null
|
||||
|
|
@ -384,7 +386,7 @@ class RustMatrixClient(
|
|||
serverNames = serverNames,
|
||||
).destroy()
|
||||
try {
|
||||
awaitJoinedRoom(roomIdOrAlias, 10.seconds)
|
||||
awaitRoom(roomIdOrAlias, 10.seconds, CurrentUserMembership.JOINED)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||
null
|
||||
|
|
@ -392,8 +394,18 @@ class RustMatrixClient(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun knockRoom(roomId: RoomId): Result<Unit> {
|
||||
return Result.failure(NotImplementedError("Not yet implemented"))
|
||||
override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?> = withContext(
|
||||
sessionDispatcher
|
||||
) {
|
||||
runCatching {
|
||||
client.knock(roomIdOrAlias.identifier, message, serverNames).destroy()
|
||||
try {
|
||||
awaitRoom(roomIdOrAlias, 10.seconds, CurrentUserMembership.KNOCKED)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ class UtdTracker(
|
|||
Timber.d("onUtd for event ${info.eventId}, timeToDecryptMs: ${info.timeToDecryptMs}")
|
||||
val name = when (info.cause) {
|
||||
UtdCause.UNKNOWN -> Error.Name.OlmKeysNotSentError
|
||||
UtdCause.MEMBERSHIP -> Error.Name.ExpectedDueToMembership
|
||||
UtdCause.SENT_BEFORE_WE_JOINED -> Error.Name.ExpectedDueToMembership
|
||||
UtdCause.VERIFICATION_VIOLATION -> Error.Name.ExpectedVerificationViolation
|
||||
UtdCause.UNSIGNED_DEVICE,
|
||||
UtdCause.UNKNOWN_DEVICE -> {
|
||||
Error.Name.ExpectedSentByInsecureDevice
|
||||
}
|
||||
}
|
||||
val event = Error(
|
||||
context = null,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import org.matrix.rustcomponents.sdk.BackupSteadyStateListener
|
|||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener
|
||||
import org.matrix.rustcomponents.sdk.Encryption
|
||||
import org.matrix.rustcomponents.sdk.UserIdentity
|
||||
import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState
|
||||
import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress
|
||||
import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException
|
||||
|
|
@ -204,8 +205,18 @@ internal class RustEncryptionService(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun isUserVerified(userId: UserId): Result<Boolean> = runCatching {
|
||||
getUserIdentity(userId).isVerified()
|
||||
}
|
||||
|
||||
override suspend fun pinUserIdentity(userId: UserId): Result<Unit> = runCatching {
|
||||
val userIdentity = service.getUserIdentity(userId.value) ?: error("User identity not found")
|
||||
userIdentity.pin()
|
||||
getUserIdentity(userId).pin()
|
||||
}
|
||||
|
||||
private suspend fun getUserIdentity(userId: UserId): UserIdentity {
|
||||
return service.userIdentity(
|
||||
userId = userId.value,
|
||||
// requestFromHomeserverIfNeeded = true,
|
||||
) ?: error("User identity not found")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,13 @@ package io.element.android.libraries.matrix.impl.room
|
|||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.RequiredState
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.RoomSubscription
|
||||
import timber.log.Timber
|
||||
|
||||
private const val DEFAULT_TIMELINE_LIMIT = 20u
|
||||
|
||||
class RoomSyncSubscriber(
|
||||
private val roomListService: RoomListService,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
|
|
@ -28,28 +23,13 @@ class RoomSyncSubscriber(
|
|||
private val subscribedRoomIds = mutableSetOf<RoomId>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
private val settings = RoomSubscription(
|
||||
requiredState = listOf(
|
||||
RequiredState(key = EventType.STATE_ROOM_NAME, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_AVATAR, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_PINNED_EVENT, value = ""),
|
||||
),
|
||||
timelineLimit = DEFAULT_TIMELINE_LIMIT,
|
||||
// We don't need heroes here as they're already included in the `all_rooms` list
|
||||
includeHeroes = false,
|
||||
)
|
||||
|
||||
suspend fun subscribe(roomId: RoomId) {
|
||||
mutex.withLock {
|
||||
withContext(dispatchers.io) {
|
||||
try {
|
||||
if (!isSubscribedTo(roomId)) {
|
||||
Timber.d("Subscribing to room $roomId}")
|
||||
roomListService.subscribeToRooms(listOf(roomId.value), settings)
|
||||
roomListService.subscribeToRooms(listOf(roomId.value))
|
||||
}
|
||||
subscribedRoomIds.add(roomId)
|
||||
} catch (exception: Exception) {
|
||||
|
|
@ -65,7 +45,7 @@ class RoomSyncSubscriber(
|
|||
val roomIdsToSubscribeTo = roomIds.filterNot { isSubscribedTo(it) }
|
||||
if (roomIdsToSubscribeTo.isNotEmpty()) {
|
||||
Timber.d("Subscribing to rooms: $roomIds")
|
||||
roomListService.subscribeToRooms(roomIdsToSubscribeTo.map { it.value }, settings)
|
||||
roomListService.subscribeToRooms(roomIdsToSubscribeTo.map { it.value })
|
||||
subscribedRoomIds.addAll(roomIds)
|
||||
}
|
||||
} catch (cancellationException: CancellationException) {
|
||||
|
|
|
|||
|
|
@ -9,20 +9,20 @@ package io.element.android.libraries.matrix.impl.room
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
||||
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
|
||||
class RustInvitedRoom(
|
||||
class RustPendingRoom(
|
||||
override val sessionId: SessionId,
|
||||
private val invitedRoom: Room,
|
||||
) : InvitedRoom {
|
||||
override val roomId = RoomId(invitedRoom.id())
|
||||
private val inner: Room,
|
||||
) : PendingRoom {
|
||||
override val roomId = RoomId(inner.id())
|
||||
|
||||
override suspend fun declineInvite(): Result<Unit> = runCatching {
|
||||
invitedRoom.leave()
|
||||
override suspend fun leave(): Result<Unit> = runCatching {
|
||||
inner.leave()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
invitedRoom.destroy()
|
||||
inner.destroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,8 @@ import io.element.android.libraries.matrix.api.core.DeviceId
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
|
||||
|
|
@ -35,6 +35,7 @@ import timber.log.Timber
|
|||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||
|
||||
private const val CACHE_SIZE = 16
|
||||
private val PENDING_MEMBERSHIPS = setOf(Membership.INVITED, Membership.KNOCKED)
|
||||
|
||||
class RustRoomFactory(
|
||||
private val sessionId: SessionId,
|
||||
|
|
@ -120,7 +121,7 @@ class RustRoomFactory(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun createInvitedRoom(roomId: RoomId): InvitedRoom? = withContext(dispatcher) {
|
||||
suspend fun createPendingRoom(roomId: RoomId): PendingRoom? = withContext(dispatcher) {
|
||||
if (isDestroyed) {
|
||||
Timber.d("Room factory is destroyed, returning null for $roomId")
|
||||
return@withContext null
|
||||
|
|
@ -130,20 +131,20 @@ class RustRoomFactory(
|
|||
Timber.d("Room not found for $roomId")
|
||||
return@withContext null
|
||||
}
|
||||
if (roomListItem.membership() != Membership.INVITED) {
|
||||
Timber.d("Room $roomId is not in invited state")
|
||||
if (roomListItem.membership() !in PENDING_MEMBERSHIPS) {
|
||||
Timber.d("Room $roomId is not in pending state")
|
||||
return@withContext null
|
||||
}
|
||||
val invitedRoom = try {
|
||||
val innerRoom = try {
|
||||
// TODO use new method when available, for now it'll fail for knocked rooms
|
||||
roomListItem.invitedRoom()
|
||||
} catch (e: RoomListException) {
|
||||
Timber.e(e, "Failed to get invited room for $roomId")
|
||||
Timber.e(e, "Failed to get pending room for $roomId")
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
RustInvitedRoom(
|
||||
RustPendingRoom(
|
||||
sessionId = sessionId,
|
||||
invitedRoom = invitedRoom,
|
||||
inner = innerRoom,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,10 +36,11 @@ object RoomMemberMapper {
|
|||
|
||||
fun mapMembership(membershipState: RustMembershipState): RoomMembershipState =
|
||||
when (membershipState) {
|
||||
RustMembershipState.BAN -> RoomMembershipState.BAN
|
||||
RustMembershipState.INVITE -> RoomMembershipState.INVITE
|
||||
RustMembershipState.JOIN -> RoomMembershipState.JOIN
|
||||
RustMembershipState.KNOCK -> RoomMembershipState.KNOCK
|
||||
RustMembershipState.LEAVE -> RoomMembershipState.LEAVE
|
||||
RustMembershipState.Ban -> RoomMembershipState.BAN
|
||||
RustMembershipState.Invite -> RoomMembershipState.INVITE
|
||||
RustMembershipState.Join -> RoomMembershipState.JOIN
|
||||
RustMembershipState.Knock -> RoomMembershipState.KNOCK
|
||||
RustMembershipState.Leave -> RoomMembershipState.LEAVE
|
||||
is RustMembershipState.Custom -> TODO()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,23 +11,28 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||
import io.element.android.libraries.matrix.impl.room.toRoomType
|
||||
import org.matrix.rustcomponents.sdk.JoinRule
|
||||
import org.matrix.rustcomponents.sdk.Membership
|
||||
import org.matrix.rustcomponents.sdk.RoomPreview as RustRoomPreview
|
||||
|
||||
object RoomPreviewMapper {
|
||||
fun map(roomPreview: RustRoomPreview): RoomPreview {
|
||||
return RoomPreview(
|
||||
roomId = RoomId(roomPreview.roomId),
|
||||
canonicalAlias = roomPreview.canonicalAlias?.let(::RoomAlias),
|
||||
name = roomPreview.name,
|
||||
topic = roomPreview.topic,
|
||||
avatarUrl = roomPreview.avatarUrl,
|
||||
numberOfJoinedMembers = roomPreview.numJoinedMembers.toLong(),
|
||||
roomType = roomPreview.roomType.toRoomType(),
|
||||
isHistoryWorldReadable = roomPreview.isHistoryWorldReadable,
|
||||
isJoined = roomPreview.isJoined,
|
||||
isInvited = roomPreview.isInvited,
|
||||
isPublic = roomPreview.isPublic,
|
||||
canKnock = roomPreview.canKnock
|
||||
)
|
||||
return roomPreview.use {
|
||||
val info = roomPreview.info()
|
||||
RoomPreview(
|
||||
roomId = RoomId(info.roomId),
|
||||
canonicalAlias = info.canonicalAlias?.let(::RoomAlias),
|
||||
name = info.name,
|
||||
topic = info.topic,
|
||||
avatarUrl = info.avatarUrl,
|
||||
numberOfJoinedMembers = info.numJoinedMembers.toLong(),
|
||||
roomType = info.roomType.toRoomType(),
|
||||
isHistoryWorldReadable = info.isHistoryWorldReadable,
|
||||
isJoined = info.membership == Membership.JOINED,
|
||||
isInvited = info.membership == Membership.INVITED,
|
||||
isPublic = info.joinRule == JoinRule.Public,
|
||||
canKnock = info.joinRule == JoinRule.Knock
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -339,6 +339,7 @@ class RustTimeline(
|
|||
formattedCaption = formattedBody?.let {
|
||||
FormattedBody(body = it, format = MessageFormat.Html)
|
||||
},
|
||||
storeInCache = true,
|
||||
progressWatcher = progressCallback?.toProgressWatcher()
|
||||
)
|
||||
}
|
||||
|
|
@ -361,6 +362,7 @@ class RustTimeline(
|
|||
formattedCaption = formattedBody?.let {
|
||||
FormattedBody(body = it, format = MessageFormat.Html)
|
||||
},
|
||||
storeInCache = true,
|
||||
progressWatcher = progressCallback?.toProgressWatcher()
|
||||
)
|
||||
}
|
||||
|
|
@ -374,6 +376,7 @@ class RustTimeline(
|
|||
// Maybe allow a caption in the future?
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
storeInCache = true,
|
||||
progressWatcher = progressCallback?.toProgressWatcher()
|
||||
)
|
||||
}
|
||||
|
|
@ -381,7 +384,7 @@ class RustTimeline(
|
|||
|
||||
override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> {
|
||||
return sendAttachment(listOf(file)) {
|
||||
inner.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher())
|
||||
inner.sendFile(file.path, fileInfo.map(), false, progressCallback?.toProgressWatcher())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -496,6 +499,7 @@ class RustTimeline(
|
|||
// Maybe allow a caption in the future?
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
storeInCache = true,
|
||||
progressWatcher = progressCallback?.toProgressWatcher(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.matrix.rustcomponents.sdk.EventOrTransactionId
|
||||
import org.matrix.rustcomponents.sdk.EventSendState
|
||||
import org.matrix.rustcomponents.sdk.QueueWedgeError
|
||||
import org.matrix.rustcomponents.sdk.Reaction
|
||||
import org.matrix.rustcomponents.sdk.ShieldState
|
||||
import uniffi.matrix_sdk_common.ShieldStateCode
|
||||
|
|
@ -78,25 +78,31 @@ fun RustEventSendState?.map(): LocalEventSendState? {
|
|||
null -> null
|
||||
RustEventSendState.NotSentYet -> LocalEventSendState.Sending
|
||||
is RustEventSendState.SendingFailed -> {
|
||||
if (isRecoverable) {
|
||||
LocalEventSendState.Sending
|
||||
} else {
|
||||
LocalEventSendState.Failed.Unknown(error)
|
||||
when (val queueWedgeError = error) {
|
||||
QueueWedgeError.CrossVerificationRequired -> {
|
||||
// The current device is not cross-signed (or cross signing is not setup)
|
||||
LocalEventSendState.Failed.SendingFromUnverifiedDevice
|
||||
}
|
||||
is QueueWedgeError.IdentityViolations -> {
|
||||
LocalEventSendState.Failed.VerifiedUserChangedIdentity(queueWedgeError.users.map { UserId(it) })
|
||||
}
|
||||
is QueueWedgeError.InsecureDevices -> {
|
||||
LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice(
|
||||
devices = queueWedgeError.userDeviceMap.entries.associate { entry ->
|
||||
UserId(entry.key) to entry.value.map { DeviceId(it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
is QueueWedgeError.GenericApiError -> {
|
||||
if (isRecoverable) {
|
||||
LocalEventSendState.Sending
|
||||
} else {
|
||||
LocalEventSendState.Failed.Unknown(queueWedgeError.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is RustEventSendState.Sent -> LocalEventSendState.Sent(EventId(eventId))
|
||||
is RustEventSendState.VerifiedUserChangedIdentity -> {
|
||||
LocalEventSendState.Failed.VerifiedUserChangedIdentity(users.map { UserId(it) })
|
||||
}
|
||||
is RustEventSendState.VerifiedUserHasUnsignedDevice -> {
|
||||
LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice(
|
||||
devices = devices.entries.associate { entry ->
|
||||
UserId(entry.key) to entry.value.map { DeviceId(it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
EventSendState.CrossSigningNotSetup -> LocalEventSendState.Failed.CrossSigningNotSetup
|
||||
EventSendState.SendingFromUnverifiedDevice -> LocalEventSendState.Failed.SendingFromUnverifiedDevice
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -140,8 +140,11 @@ private fun RustMembershipChange.map(): MembershipChange {
|
|||
|
||||
private fun RustUtdCause.map(): UtdCause {
|
||||
return when (this) {
|
||||
RustUtdCause.MEMBERSHIP -> UtdCause.Membership
|
||||
RustUtdCause.SENT_BEFORE_WE_JOINED -> UtdCause.SentBeforeWeJoined
|
||||
RustUtdCause.UNKNOWN -> UtdCause.Unknown
|
||||
RustUtdCause.VERIFICATION_VIOLATION -> UtdCause.VerificationViolation
|
||||
RustUtdCause.UNSIGNED_DEVICE -> UtdCause.UnsignedDevice
|
||||
RustUtdCause.UNKNOWN_DEVICE -> UtdCause.UnknownDevice
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,15 @@ package io.element.android.libraries.matrix.impl.verification
|
|||
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -28,6 +31,7 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.Encryption
|
||||
|
|
@ -41,6 +45,7 @@ import org.matrix.rustcomponents.sdk.use
|
|||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
|
||||
|
||||
class RustSessionVerificationService(
|
||||
private val client: Client,
|
||||
|
|
@ -100,6 +105,16 @@ class RustSessionVerificationService(
|
|||
.launchIn(sessionCoroutineScope)
|
||||
}
|
||||
|
||||
override fun didReceiveVerificationRequest(details: RustSessionVerificationRequestDetails) {
|
||||
listener?.onIncomingSessionRequest(details.map())
|
||||
}
|
||||
|
||||
private var listener: SessionVerificationServiceListener? = null
|
||||
|
||||
override fun setListener(listener: SessionVerificationServiceListener?) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
override suspend fun requestVerification() = tryOrFail {
|
||||
initVerificationControllerIfNeeded()
|
||||
verificationController.requestVerification()
|
||||
|
|
@ -119,9 +134,24 @@ class RustSessionVerificationService(
|
|||
verificationController.startSasVerification()
|
||||
}
|
||||
|
||||
override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) = tryOrFail {
|
||||
verificationController.acknowledgeVerificationRequest(
|
||||
senderId = details.senderId.value,
|
||||
flowId = details.flowId.value,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun acceptVerificationRequest() = tryOrFail {
|
||||
verificationController.acceptVerificationRequest()
|
||||
}
|
||||
|
||||
private suspend fun tryOrFail(block: suspend () -> Unit) {
|
||||
runCatching {
|
||||
block()
|
||||
// Ensure the block cannot be cancelled, else if the Rust SDK emit a new state during the API execution,
|
||||
// the state machine may cancel the api call.
|
||||
withContext(NonCancellable) {
|
||||
block()
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to verify session")
|
||||
didFail()
|
||||
|
|
@ -132,16 +162,16 @@ class RustSessionVerificationService(
|
|||
|
||||
// When verification attempt is accepted by the other device
|
||||
override fun didAcceptVerificationRequest() {
|
||||
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
|
||||
_verificationFlowState.value = VerificationFlowState.DidAcceptVerificationRequest
|
||||
}
|
||||
|
||||
override fun didCancel() {
|
||||
_verificationFlowState.value = VerificationFlowState.Canceled
|
||||
_verificationFlowState.value = VerificationFlowState.DidCancel
|
||||
}
|
||||
|
||||
override fun didFail() {
|
||||
Timber.e("Session verification failed with an unknown error")
|
||||
_verificationFlowState.value = VerificationFlowState.Failed
|
||||
_verificationFlowState.value = VerificationFlowState.DidFail
|
||||
}
|
||||
|
||||
override fun didFinish() {
|
||||
|
|
@ -150,14 +180,14 @@ class RustSessionVerificationService(
|
|||
// It also sometimes unexpectedly fails to report the session as verified, so we have to handle that possibility and fail if needed
|
||||
runCatching {
|
||||
withTimeout(30.seconds) {
|
||||
while (!verificationController.isVerified()) {
|
||||
while (encryptionService.verificationState() != VerificationState.VERIFIED) {
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onSuccess {
|
||||
// Order here is important, first set the flow state as finished, then update the verification status
|
||||
_verificationFlowState.value = VerificationFlowState.Finished
|
||||
_verificationFlowState.value = VerificationFlowState.DidFinish
|
||||
updateVerificationStatus()
|
||||
}
|
||||
.onFailure {
|
||||
|
|
@ -168,18 +198,18 @@ class RustSessionVerificationService(
|
|||
}
|
||||
|
||||
override fun didReceiveVerificationData(data: RustSessionVerificationData) {
|
||||
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(data.map())
|
||||
_verificationFlowState.value = VerificationFlowState.DidReceiveVerificationData(data.map())
|
||||
}
|
||||
|
||||
// When the actual SAS verification starts
|
||||
override fun didStartSasVerification() {
|
||||
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
|
||||
_verificationFlowState.value = VerificationFlowState.DidStartSasVerification
|
||||
}
|
||||
|
||||
// end-region
|
||||
|
||||
override suspend fun reset() {
|
||||
if (isReady.value) {
|
||||
override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) {
|
||||
if (isReady.value && cancelAnyPendingVerificationAttempt) {
|
||||
// Cancel any pending verification attempt
|
||||
tryOrNull { verificationController.cancelVerification() }
|
||||
}
|
||||
|
|
@ -208,7 +238,7 @@ class RustSessionVerificationService(
|
|||
}
|
||||
|
||||
private suspend fun updateVerificationStatus() {
|
||||
if (verificationFlowState.value == VerificationFlowState.Finished) {
|
||||
if (verificationFlowState.value == VerificationFlowState.DidFinish) {
|
||||
// Calling `encryptionService.verificationState()` performs a network call and it will deadlock if there is no network
|
||||
// So we need to check that *only* if we know there is network connection, which is the case when the verification flow just finished
|
||||
Timber.d("Updating verification status: flow just finished")
|
||||
|
|
@ -227,7 +257,7 @@ class RustSessionVerificationService(
|
|||
Timber.d("Updating verification status: flow is pending or was finished some time ago")
|
||||
runCatching {
|
||||
initVerificationControllerIfNeeded()
|
||||
_sessionVerifiedStatus.value = if (verificationController.isVerified()) {
|
||||
_sessionVerifiedStatus.value = if (encryptionService.verificationState() == VerificationState.VERIFIED) {
|
||||
SessionVerifiedStatus.Verified
|
||||
} else {
|
||||
SessionVerifiedStatus.NotVerified
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.verification
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.FlowId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
|
||||
|
||||
fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails(
|
||||
senderId = UserId(senderId),
|
||||
flowId = FlowId(flowId),
|
||||
deviceId = DeviceId(deviceId),
|
||||
displayName = displayName,
|
||||
firstSeenTimestamp = firstSeenTimestamp.toLong(),
|
||||
)
|
||||
|
|
@ -74,7 +74,7 @@ class UtdTrackerTest {
|
|||
UnableToDecryptInfo(
|
||||
eventId = AN_EVENT_ID.value,
|
||||
timeToDecryptMs = 123.toULong(),
|
||||
cause = UtdCause.MEMBERSHIP,
|
||||
cause = UtdCause.SENT_BEFORE_WE_JOINED,
|
||||
)
|
||||
)
|
||||
assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
|
||||
|
|
@ -90,4 +90,50 @@ class UtdTrackerTest {
|
|||
assertThat(fakeAnalyticsService.screenEvents).isEmpty()
|
||||
assertThat(fakeAnalyticsService.trackedErrors).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when onUtd is called with insecure cause, the expected analytics Event is sent`() {
|
||||
val fakeAnalyticsService = FakeAnalyticsService()
|
||||
val sut = UtdTracker(fakeAnalyticsService)
|
||||
sut.onUtd(
|
||||
UnableToDecryptInfo(
|
||||
eventId = AN_EVENT_ID.value,
|
||||
timeToDecryptMs = 123.toULong(),
|
||||
cause = UtdCause.UNSIGNED_DEVICE,
|
||||
)
|
||||
)
|
||||
assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
|
||||
Error(
|
||||
context = null,
|
||||
cryptoModule = Error.CryptoModule.Rust,
|
||||
cryptoSDK = Error.CryptoSDK.Rust,
|
||||
timeToDecryptMillis = 123,
|
||||
domain = Error.Domain.E2EE,
|
||||
name = Error.Name.ExpectedSentByInsecureDevice
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when onUtd is called with verification violation cause, the expected analytics Event is sent`() {
|
||||
val fakeAnalyticsService = FakeAnalyticsService()
|
||||
val sut = UtdTracker(fakeAnalyticsService)
|
||||
sut.onUtd(
|
||||
UnableToDecryptInfo(
|
||||
eventId = AN_EVENT_ID.value,
|
||||
timeToDecryptMs = 123.toULong(),
|
||||
cause = UtdCause.VERIFICATION_VIOLATION,
|
||||
)
|
||||
)
|
||||
assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
|
||||
Error(
|
||||
context = null,
|
||||
cryptoModule = Error.CryptoModule.Rust,
|
||||
cryptoSDK = Error.CryptoSDK.Rust,
|
||||
timeToDecryptMillis = 123,
|
||||
domain = Error.Domain.E2EE,
|
||||
name = Error.Name.ExpectedVerificationViolation
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ fun aRustRoomMember(
|
|||
userId: UserId,
|
||||
displayName: String? = null,
|
||||
avatarUrl: String? = null,
|
||||
membership: MembershipState = MembershipState.JOIN,
|
||||
membership: MembershipState = MembershipState.Join,
|
||||
isNameAmbiguous: Boolean = false,
|
||||
powerLevel: Long = 0L,
|
||||
isIgnored: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -9,16 +9,16 @@ package io.element.android.libraries.matrix.impl.fixtures.factories
|
|||
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import org.matrix.rustcomponents.sdk.RoomPreview
|
||||
import org.matrix.rustcomponents.sdk.JoinRule
|
||||
import org.matrix.rustcomponents.sdk.Membership
|
||||
import org.matrix.rustcomponents.sdk.RoomPreviewInfo
|
||||
|
||||
internal fun aRustRoomPreview(
|
||||
internal fun aRustRoomPreviewInfo(
|
||||
canonicalAlias: String? = A_ROOM_ALIAS.value,
|
||||
isJoined: Boolean = true,
|
||||
isInvited: Boolean = true,
|
||||
isPublic: Boolean = true,
|
||||
canKnock: Boolean = true,
|
||||
): RoomPreview {
|
||||
return RoomPreview(
|
||||
membership: Membership? = Membership.JOINED,
|
||||
joinRule: JoinRule = JoinRule.Public,
|
||||
): RoomPreviewInfo {
|
||||
return RoomPreviewInfo(
|
||||
roomId = A_ROOM_ID.value,
|
||||
canonicalAlias = canonicalAlias,
|
||||
name = "name",
|
||||
|
|
@ -27,9 +27,7 @@ internal fun aRustRoomPreview(
|
|||
numJoinedMembers = 1u,
|
||||
roomType = null,
|
||||
isHistoryWorldReadable = true,
|
||||
isJoined = isJoined,
|
||||
isInvited = isInvited,
|
||||
isPublic = isPublic,
|
||||
canKnock = canKnock,
|
||||
membership = membership,
|
||||
joinRule = joinRule,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreviewInfo
|
||||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.RoomPreview
|
||||
import org.matrix.rustcomponents.sdk.RoomPreviewInfo
|
||||
|
||||
class FakeRustRoomPreview(
|
||||
private val info: RoomPreviewInfo = aRustRoomPreviewInfo(),
|
||||
) : RoomPreview(NoPointer) {
|
||||
override fun info(): RoomPreviewInfo {
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
|
@ -24,10 +24,10 @@ class RoomMemberMapperTest {
|
|||
|
||||
@Test
|
||||
fun mapMembership() {
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.BAN)).isEqualTo(RoomMembershipState.BAN)
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.INVITE)).isEqualTo(RoomMembershipState.INVITE)
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.JOIN)).isEqualTo(RoomMembershipState.JOIN)
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.KNOCK)).isEqualTo(RoomMembershipState.KNOCK)
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.LEAVE)).isEqualTo(RoomMembershipState.LEAVE)
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.Ban)).isEqualTo(RoomMembershipState.BAN)
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.Invite)).isEqualTo(RoomMembershipState.INVITE)
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.Join)).isEqualTo(RoomMembershipState.JOIN)
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.Knock)).isEqualTo(RoomMembershipState.KNOCK)
|
||||
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.Leave)).isEqualTo(RoomMembershipState.LEAVE)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,23 @@ package io.element.android.libraries.matrix.impl.room.preview
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreview
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreviewInfo
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomPreview
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.JoinRule
|
||||
import org.matrix.rustcomponents.sdk.Membership
|
||||
|
||||
class RoomPreviewMapperTest {
|
||||
@Test
|
||||
fun `map should map values 1`() {
|
||||
assertThat(
|
||||
RoomPreviewMapper.map(
|
||||
aRustRoomPreview(
|
||||
isJoined = false,
|
||||
isInvited = false,
|
||||
FakeRustRoomPreview(
|
||||
info = aRustRoomPreviewInfo(
|
||||
membership = null,
|
||||
)
|
||||
)
|
||||
)
|
||||
).isEqualTo(
|
||||
|
|
@ -38,7 +42,7 @@ class RoomPreviewMapperTest {
|
|||
isJoined = false,
|
||||
isInvited = false,
|
||||
isPublic = true,
|
||||
canKnock = true,
|
||||
canKnock = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -47,10 +51,12 @@ class RoomPreviewMapperTest {
|
|||
fun `map should map values 2`() {
|
||||
assertThat(
|
||||
RoomPreviewMapper.map(
|
||||
aRustRoomPreview(
|
||||
canonicalAlias = null,
|
||||
isPublic = false,
|
||||
canKnock = false,
|
||||
FakeRustRoomPreview(
|
||||
info = aRustRoomPreviewInfo(
|
||||
canonicalAlias = null,
|
||||
membership = Membership.JOINED,
|
||||
joinRule = JoinRule.Knock,
|
||||
)
|
||||
)
|
||||
)
|
||||
).isEqualTo(
|
||||
|
|
@ -64,9 +70,9 @@ class RoomPreviewMapperTest {
|
|||
roomType = RoomType.Room,
|
||||
isHistoryWorldReadable = true,
|
||||
isJoined = true,
|
||||
isInvited = true,
|
||||
isInvited = false,
|
||||
isPublic = false,
|
||||
canKnock = false,
|
||||
canKnock = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
|
|||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||
|
|
@ -101,7 +101,7 @@ class FakeMatrixClient(
|
|||
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||
private var findDmResult: RoomId? = A_ROOM_ID
|
||||
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
||||
val getInvitedRoomResults = mutableMapOf<RoomId, InvitedRoom>()
|
||||
val getPendingRoomResults = mutableMapOf<RoomId, PendingRoom>()
|
||||
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
||||
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
|
||||
private var uploadMediaResult: Result<String> = Result.success(AN_AVATAR_URL)
|
||||
|
|
@ -114,8 +114,8 @@ class FakeMatrixClient(
|
|||
var joinRoomByIdOrAliasLambda: (RoomIdOrAlias, List<String>) -> Result<RoomSummary?> = { _, _ ->
|
||||
Result.success(null)
|
||||
}
|
||||
var knockRoomLambda: (RoomId) -> Result<Unit> = {
|
||||
Result.success(Unit)
|
||||
var knockRoomLambda: (RoomIdOrAlias, String, List<String>) -> Result<RoomSummary?> = { _, _, _ ->
|
||||
Result.success(null)
|
||||
}
|
||||
var getRoomSummaryFlowLambda = { _: RoomIdOrAlias ->
|
||||
flowOf<Optional<RoomSummary>>(Optional.empty())
|
||||
|
|
@ -128,8 +128,8 @@ class FakeMatrixClient(
|
|||
return getRoomResults[roomId]
|
||||
}
|
||||
|
||||
override suspend fun getInvitedRoom(roomId: RoomId): InvitedRoom? {
|
||||
return getInvitedRoomResults[roomId]
|
||||
override suspend fun getPendingRoom(roomId: RoomId): PendingRoom? {
|
||||
return getPendingRoomResults[roomId]
|
||||
}
|
||||
|
||||
override suspend fun findDM(userId: UserId): RoomId? {
|
||||
|
|
@ -223,7 +223,9 @@ class FakeMatrixClient(
|
|||
return joinRoomByIdOrAliasLambda(roomIdOrAlias, serverNames)
|
||||
}
|
||||
|
||||
override suspend fun knockRoom(roomId: RoomId): Result<Unit> = knockRoomLambda(roomId)
|
||||
override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?> {
|
||||
return knockRoomLambda(roomIdOrAlias, message, serverNames)
|
||||
}
|
||||
|
||||
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.flowOf
|
|||
class FakeEncryptionService(
|
||||
var startIdentityResetLambda: () -> Result<IdentityResetHandle?> = { lambdaError() },
|
||||
private val pinUserIdentityResult: (UserId) -> Result<Unit> = { lambdaError() },
|
||||
private val isUserVerifiedResult: (UserId) -> Result<Boolean> = { lambdaError() },
|
||||
) : EncryptionService {
|
||||
private var disableRecoveryFailure: Exception? = null
|
||||
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
|
||||
|
|
@ -123,6 +124,10 @@ class FakeEncryptionService(
|
|||
return pinUserIdentityResult(userId)
|
||||
}
|
||||
|
||||
override suspend fun isUserVerified(userId: UserId): Result<Boolean> = simulateLongTask {
|
||||
isUserVerifiedResult(userId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FAKE_RECOVERY_KEY = "fake"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,18 @@ package io.element.android.libraries.matrix.test.room
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.InvitedRoom
|
||||
import io.element.android.libraries.matrix.api.room.PendingRoom
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeInvitedRoom(
|
||||
class FakePendingRoom(
|
||||
override val sessionId: SessionId = A_SESSION_ID,
|
||||
override val roomId: RoomId = A_ROOM_ID,
|
||||
private val declineInviteResult: () -> Result<Unit> = { lambdaError() }
|
||||
) : InvitedRoom {
|
||||
override suspend fun declineInvite(): Result<Unit> = simulateLongTask {
|
||||
) : PendingRoom {
|
||||
override suspend fun leave(): Result<Unit> = simulateLongTask {
|
||||
declineInviteResult()
|
||||
}
|
||||
|
||||
|
|
@ -7,79 +7,84 @@
|
|||
|
||||
package io.element.android.libraries.matrix.test.verification
|
||||
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class FakeSessionVerificationService(
|
||||
initialSessionVerifiedStatus: SessionVerifiedStatus = SessionVerifiedStatus.Unknown,
|
||||
private val requestVerificationLambda: () -> Unit = { lambdaError() },
|
||||
private val cancelVerificationLambda: () -> Unit = { lambdaError() },
|
||||
private val approveVerificationLambda: () -> Unit = { lambdaError() },
|
||||
private val declineVerificationLambda: () -> Unit = { lambdaError() },
|
||||
private val startVerificationLambda: () -> Unit = { lambdaError() },
|
||||
private val resetLambda: (Boolean) -> Unit = { lambdaError() },
|
||||
private val acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() },
|
||||
private val acceptVerificationRequestLambda: () -> Unit = { lambdaError() },
|
||||
) : SessionVerificationService {
|
||||
private val _sessionVerifiedStatus = MutableStateFlow(initialSessionVerifiedStatus)
|
||||
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
||||
private var _needsSessionVerification = MutableStateFlow(true)
|
||||
var shouldFail = false
|
||||
|
||||
override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
|
||||
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
|
||||
override val needsSessionVerification: Flow<Boolean> = _needsSessionVerification
|
||||
|
||||
override suspend fun requestVerification() {
|
||||
if (!shouldFail) {
|
||||
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
|
||||
} else {
|
||||
_verificationFlowState.value = VerificationFlowState.Failed
|
||||
}
|
||||
requestVerificationLambda()
|
||||
}
|
||||
|
||||
override suspend fun cancelVerification() {
|
||||
_verificationFlowState.value = VerificationFlowState.Canceled
|
||||
cancelVerificationLambda()
|
||||
}
|
||||
|
||||
override suspend fun approveVerification() {
|
||||
if (!shouldFail) {
|
||||
_verificationFlowState.value = VerificationFlowState.Finished
|
||||
} else {
|
||||
_verificationFlowState.value = VerificationFlowState.Failed
|
||||
}
|
||||
approveVerificationLambda()
|
||||
}
|
||||
|
||||
override suspend fun declineVerification() {
|
||||
if (!shouldFail) {
|
||||
_verificationFlowState.value = VerificationFlowState.Canceled
|
||||
} else {
|
||||
_verificationFlowState.value = VerificationFlowState.Failed
|
||||
}
|
||||
}
|
||||
|
||||
fun triggerReceiveVerificationData(sessionVerificationData: SessionVerificationData) {
|
||||
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(sessionVerificationData)
|
||||
declineVerificationLambda()
|
||||
}
|
||||
|
||||
override suspend fun startVerification() {
|
||||
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
|
||||
startVerificationLambda()
|
||||
}
|
||||
|
||||
fun givenVerifiedStatus(status: SessionVerifiedStatus) {
|
||||
_sessionVerifiedStatus.value = status
|
||||
override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) {
|
||||
resetLambda(cancelAnyPendingVerificationAttempt)
|
||||
}
|
||||
|
||||
var listener: SessionVerificationServiceListener? = null
|
||||
private set
|
||||
|
||||
override fun setListener(listener: SessionVerificationServiceListener?) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) {
|
||||
acknowledgeVerificationRequestLambda(details)
|
||||
}
|
||||
|
||||
override suspend fun acceptVerificationRequest() = simulateLongTask {
|
||||
acceptVerificationRequestLambda()
|
||||
}
|
||||
|
||||
suspend fun emitVerificationFlowState(state: VerificationFlowState) {
|
||||
_verificationFlowState.emit(state)
|
||||
}
|
||||
|
||||
suspend fun emitVerifiedStatus(status: SessionVerifiedStatus) {
|
||||
_sessionVerifiedStatus.emit(status)
|
||||
}
|
||||
|
||||
fun givenVerificationFlowState(state: VerificationFlowState) {
|
||||
_verificationFlowState.value = state
|
||||
}
|
||||
|
||||
fun givenNeedsSessionVerification(needsVerification: Boolean) {
|
||||
_needsSessionVerification.value = needsVerification
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
_verificationFlowState.value = VerificationFlowState.Initial
|
||||
suspend fun emitNeedsSessionVerification(needsVerification: Boolean) {
|
||||
_needsSessionVerification.emit(needsVerification)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@
|
|||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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
|
||||
|
|
@ -30,11 +31,12 @@ fun InviteSenderView(
|
|||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(modifier = Modifier.padding(vertical = 2.dp)) {
|
||||
Avatar(avatarData = inviteSender.avatarData)
|
||||
}
|
||||
Text(
|
||||
text = inviteSender.annotatedString(),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
api(projects.libraries.matrix.api)
|
||||
api(projects.libraries.preferences.api)
|
||||
implementation(libs.inject)
|
||||
implementation(libs.coroutines.core)
|
||||
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.test.junit)
|
||||
|
|
|
|||
|
|
@ -12,14 +12,17 @@ import io.element.android.libraries.core.extensions.flatMapCatching
|
|||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
class MediaSender @Inject constructor(
|
||||
private val preProcessor: MediaPreProcessor,
|
||||
private val room: MatrixRoom,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
) {
|
||||
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
||||
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
||||
|
|
@ -27,11 +30,11 @@ class MediaSender @Inject constructor(
|
|||
suspend fun sendMedia(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
compressIfPossible: Boolean,
|
||||
caption: String? = null,
|
||||
formattedCaption: String? = null,
|
||||
progressCallback: ProgressCallback? = null
|
||||
): Result<Unit> {
|
||||
val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
|
||||
return preProcessor
|
||||
.process(
|
||||
uri = uri,
|
||||
|
|
@ -49,6 +52,7 @@ class MediaSender @Inject constructor(
|
|||
}
|
||||
.handleSendResult()
|
||||
}
|
||||
|
||||
suspend fun sendVoiceMessage(
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
|
|
@ -60,7 +64,7 @@ class MediaSender @Inject constructor(
|
|||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = true,
|
||||
compressIfPossible = false
|
||||
compressIfPossible = false,
|
||||
)
|
||||
.flatMapCatching { info ->
|
||||
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -33,7 +35,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(preProcessor)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
|
||||
assertThat(preProcessor.processCallCount).isEqualTo(1)
|
||||
}
|
||||
|
|
@ -49,7 +51,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(room = room)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
sendMediaResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +63,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(preProcessor)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
|
||||
assertThat(result.exceptionOrNull()).isNotNull()
|
||||
}
|
||||
|
|
@ -74,7 +76,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(room = room)
|
||||
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
|
||||
assertThat(result.exceptionOrNull()).isNotNull()
|
||||
}
|
||||
|
|
@ -88,7 +90,7 @@ class MediaSenderTest {
|
|||
val sender = aMediaSender(room = room)
|
||||
val sendJob = launch {
|
||||
val uri = Uri.parse("content://image.jpg")
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
|
||||
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
|
||||
}
|
||||
// Wait until several internal tasks run and the file is being uploaded
|
||||
advanceTimeBy(3L)
|
||||
|
|
@ -109,8 +111,10 @@ class MediaSenderTest {
|
|||
private fun aMediaSender(
|
||||
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||
room: MatrixRoom = FakeMatrixRoom(),
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
) = MediaSender(
|
||||
preProcessor,
|
||||
room,
|
||||
preProcessor = preProcessor,
|
||||
room = room,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ImageCompressor @Inject constructor(
|
|||
resizeMode: ResizeMode,
|
||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
||||
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||
desiredQuality: Int = 80,
|
||||
desiredQuality: Int = 78,
|
||||
): Result<ImageCompressionResult> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow()
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class VideoCompressor @Inject constructor(
|
|||
val future = Transcoder.into(tmpFile.path)
|
||||
.setVideoTrackStrategy(
|
||||
DefaultVideoStrategy.Builder()
|
||||
.addResizer(AtMostResizer(1920, 1080))
|
||||
.addResizer(AtMostResizer(720, 480))
|
||||
.build()
|
||||
)
|
||||
.addDataSource(context, uri)
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ class AndroidMediaPreProcessorTest {
|
|||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 114_867,
|
||||
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4567),
|
||||
size = 109_908,
|
||||
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4484),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K13]7q%zWC00R4of%\$baad"
|
||||
)
|
||||
|
|
@ -84,7 +84,7 @@ class AndroidMediaPreProcessorTest {
|
|||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 114_867,
|
||||
size = 109_908,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
|
|
|
|||
|
|
@ -28,5 +28,8 @@ interface SessionPreferencesStore {
|
|||
suspend fun setSkipSessionVerification(skip: Boolean)
|
||||
fun isSessionVerificationSkipped(): Flow<Boolean>
|
||||
|
||||
suspend fun setCompressMedia(compress: Boolean)
|
||||
fun doesCompressMedia(): Flow<Boolean>
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class DefaultSessionPreferencesStore(
|
|||
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
|
||||
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
|
||||
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
|
||||
private val compressMedia = booleanPreferencesKey("compressMedia")
|
||||
|
||||
private val dataStoreFile = storeFile(context, sessionId)
|
||||
private val store = PreferenceDataStoreFactory.create(
|
||||
|
|
@ -81,6 +82,9 @@ class DefaultSessionPreferencesStore(
|
|||
override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
|
||||
override fun isSessionVerificationSkipped(): Flow<Boolean> = get(skipSessionVerification) { false }
|
||||
|
||||
override suspend fun setCompressMedia(compress: Boolean) = update(compressMedia, compress)
|
||||
override fun doesCompressMedia(): Flow<Boolean> = get(compressMedia) { false }
|
||||
|
||||
override suspend fun clear() {
|
||||
dataStoreFile.safeDelete()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class InMemorySessionPreferencesStore(
|
|||
isSendTypingNotificationsEnabled: Boolean = true,
|
||||
isRenderTypingNotificationsEnabled: Boolean = true,
|
||||
isSessionVerificationSkipped: Boolean = false,
|
||||
doesCompressMedia: Boolean = false,
|
||||
) : SessionPreferencesStore {
|
||||
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
|
||||
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
||||
|
|
@ -25,6 +26,7 @@ class InMemorySessionPreferencesStore(
|
|||
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
|
||||
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
|
||||
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
|
||||
private val doesCompressMedia = MutableStateFlow(doesCompressMedia)
|
||||
var clearCallCount = 0
|
||||
private set
|
||||
|
||||
|
|
@ -66,6 +68,10 @@ class InMemorySessionPreferencesStore(
|
|||
return isSessionVerificationSkipped
|
||||
}
|
||||
|
||||
override suspend fun setCompressMedia(compress: Boolean) = doesCompressMedia.emit(compress)
|
||||
|
||||
override fun doesCompressMedia(): Flow<Boolean> = doesCompressMedia
|
||||
|
||||
override suspend fun clear() {
|
||||
clearCallCount++
|
||||
isSendPublicReadReceiptsEnabled.tryEmit(true)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<string name="notification_room_action_quick_reply">"Balas cepat"</string>
|
||||
<string name="notification_room_invite_body">"Mengundang Anda untuk bergabung ke ruangan"</string>
|
||||
<string name="notification_sender_me">"Saya"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s disebut atau dibalas"</string>
|
||||
<string name="notification_test_push_notification_content">"Anda sedang melihat pemberitahuan ini! Klik saya!"</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>
|
||||
|
|
|
|||
|
|
@ -303,13 +303,6 @@
|
|||
<string name="screen_room_details_pinned_events_row_title">"Замацаваныя паведамленні"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не ўдалося атрымаць інфармацыю пра карыстальніка"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Заблакіраваць"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Заблакіраваныя карыстальнікі не змогуць адпраўляць вам паведамленні, і ўсе іх паведамленні будуць схаваны. Вы можаце разблакіраваць іх у любы час."</string>
|
||||
<string name="screen_room_member_details_block_user">"Заблакіраваць карыстальніка"</string>
|
||||
<string name="screen_room_member_details_title">"Профіль"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Разблакіраваць"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Вы зноў зможаце ўбачыць усе паведамленні."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Разблакіраваць карыстальніка"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s з %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Замацаваныя паведамленні"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Загрузка паведамлення…"</string>
|
||||
|
|
|
|||
|
|
@ -198,10 +198,6 @@
|
|||
<string name="invite_friends_rich_title">"🔐️ Присъединете се към мен в %1$s"</string>
|
||||
<string name="invite_friends_text">"Хей, говорете с мен в %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Блокиране"</string>
|
||||
<string name="screen_room_member_details_block_user">"Блокиране на потребителя"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Отблокиране"</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Отблокиране на потребителя"</string>
|
||||
<string name="screen_share_location_title">"Споделяне на местоположение"</string>
|
||||
<string name="screen_share_my_location_action">"Споделяне на моето местоположение"</string>
|
||||
<string name="screen_share_open_apple_maps">"Отваряне в Apple Maps"</string>
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@
|
|||
<string name="common_edited_suffix">"(upraveno)"</string>
|
||||
<string name="common_editing">"Úpravy"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption">"Šifrování"</string>
|
||||
<string name="common_encryption_enabled">"Šifrování povoleno"</string>
|
||||
<string name="common_enter_your_pin">"Zadejte svůj PIN"</string>
|
||||
<string name="common_error">"Chyba"</string>
|
||||
|
|
@ -242,7 +243,9 @@ Důvod: %1$s."</string>
|
|||
<string name="common_topic">"Téma"</string>
|
||||
<string name="common_topic_placeholder">"O čem je tato místnost?"</string>
|
||||
<string name="common_unable_to_decrypt">"Nelze dešifrovat"</string>
|
||||
<string name="common_unable_to_decrypt_insecure_device">"Šifrováno nezabezpečeným zařízením"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"Nemáte přístup k této zprávě"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"Ověřená identita odesílatele se změnila"</string>
|
||||
<string name="common_unable_to_invite_message">"Pozvánky nebylo možné odeslat jednomu nebo více uživatelům."</string>
|
||||
<string name="common_unable_to_invite_title">"Nelze odeslat pozvánky"</string>
|
||||
<string name="common_unlock">"Odemknout"</string>
|
||||
|
|
@ -295,13 +298,9 @@ Důvod: %1$s."</string>
|
|||
<string name="screen_create_room_access_section_header">"Přístup do místnosti"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_description">"Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_title">"Požádat o připojení"</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Zrušit žádost"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ano, zrušit"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Opravdu chcete zrušit svou žádost o vstup do této místnosti?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Zrušit žádost o vstup"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Zpráva (nepovinné)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Pokud bude váš požadavek přijat, obdržíte pozvánku na vstup do místnosti."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Žádost o vstup odeslána"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Aby byla tato místnost viditelná v adresáři veřejných místností, budete potřebovat adresu místnosti."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Adresa místnosti"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Viditelnost místnosti"</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_media_upload_preview_error_failed_processing">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
|
|
@ -326,20 +325,12 @@ Důvod: %1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Připnuté zprávy"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nepodařilo se načíst údaje o uživateli"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Zablokovat"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat."</string>
|
||||
<string name="screen_room_member_details_block_user">"Zablokovat uživatele"</string>
|
||||
<string name="screen_room_member_details_title">"Profil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Odblokovat"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Odblokovat uživatele"</string>
|
||||
<string name="screen_room_member_details_verify_button_subtitle">"K ověření tohoto uživatele použijte webovou aplikaci."</string>
|
||||
<string name="screen_room_member_details_verify_button_title">"Ověřit %1$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s z %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Připnuté zprávy"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Načítání zprávy…"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"Zobrazit vše"</string>
|
||||
<string name="screen_room_title">"Chat"</string>
|
||||
<string name="screen_roomlist_knock_event_sent_description">"Žádost o vstup odeslána"</string>
|
||||
<string name="screen_share_location_title">"Sdílet polohu"</string>
|
||||
<string name="screen_share_my_location_action">"Sdílet moji polohu"</string>
|
||||
<string name="screen_share_open_apple_maps">"Otevřít v Mapách Apple"</string>
|
||||
|
|
|
|||
|
|
@ -294,13 +294,6 @@ Grund: %1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Fixierte Nachrichten"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Benutzerdetails konnten nicht abgerufen werden"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blockieren"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Blockierte Benutzer können Dir keine Nachrichten senden und alle ihre alten Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden."</string>
|
||||
<string name="screen_room_member_details_block_user">"Benutzer blockieren"</string>
|
||||
<string name="screen_room_member_details_title">"Profil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Blockierung aufheben"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Blockierung aufheben"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s von %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s fixierte Nachrichten"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Nachricht wird geladen…"</string>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@
|
|||
<string name="action_forgot_password">"Ξέχασες τον κωδικό πρόσβασης;"</string>
|
||||
<string name="action_forward">"Προώθηση"</string>
|
||||
<string name="action_go_back">"Πήγαινε πίσω"</string>
|
||||
<string name="action_ignore">"Παράβλεψη"</string>
|
||||
<string name="action_invite">"Πρόσκληση"</string>
|
||||
<string name="action_invite_friends">"Πρόσκληση ατόμων"</string>
|
||||
<string name="action_invite_friends_to_app">"Πρόσκληση ατόμων στο %1$s"</string>
|
||||
|
|
@ -138,6 +139,7 @@
|
|||
<string name="common_dark">"Σκοτεινό"</string>
|
||||
<string name="common_decryption_error">"Σφάλμα αποκρυπτογράφησης"</string>
|
||||
<string name="common_developer_options">"Επιλογές προγραμματιστή"</string>
|
||||
<string name="common_device_id">"ID συσκευής"</string>
|
||||
<string name="common_direct_chat">"Άμεση συνομιλία"</string>
|
||||
<string name="common_do_not_show_this_again">"Να μην εμφανιστεί ξανά"</string>
|
||||
<string name="common_edited_suffix">"(επεξεργάστηκε)"</string>
|
||||
|
|
@ -288,7 +290,6 @@
|
|||
<string name="screen_create_room_access_section_header">"Πρόσβαση Δωματίου"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_description">"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_title">"Αίτημα συμμετοχής"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Μήνυμα (προαιρετικό)"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Αποτυχία μεταφόρτωσης πολυμέσων, δοκίμασε ξανά."</string>
|
||||
|
|
@ -312,13 +313,6 @@
|
|||
<string name="screen_room_details_pinned_events_row_title">"Καρφιτσωμένα μηνύματα"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Δεν ήταν δυνατή η ανάκτηση στοιχείων χρήστη"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Αποκλεισμός"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Οι αποκλεισμένοι χρήστες δεν θα μπορούν να σου στέλνουν μηνύματα και όλα τα μηνύματά τους θα είναι κρυμμένα. Μπορείς να τα ξεμπλοκάρεις ανά πάσα στιγμή."</string>
|
||||
<string name="screen_room_member_details_block_user">"Αποκλεισμός χρήστη"</string>
|
||||
<string name="screen_room_member_details_title">"Προφίλ"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Άρση αποκλεισμού"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Θα μπορείς να δεις ξανά όλα τα μηνύματα του."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Κατάργηση αποκλεισμού χρήστη"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s από %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Καρφιτσωμένα μηνύματα"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Φόρτωση μηνύματος…"</string>
|
||||
|
|
|
|||
|
|
@ -250,12 +250,6 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"Error al subir el contenido multimedia, por favor inténtalo de nuevo."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Error al procesar el contenido multimedia, por favor inténtalo de nuevo."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"No se pudieron recuperar los detalles del usuario"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Bloquear"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y todos sus mensajes se ocultarán. Puedes desbloquearlos cuando quieras."</string>
|
||||
<string name="screen_room_member_details_block_user">"Bloquear usuario"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Desbloquear"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Podrás ver todos sus mensajes de nuevo."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Desbloquear usuario"</string>
|
||||
<string name="screen_share_location_title">"Compartir ubicación"</string>
|
||||
<string name="screen_share_my_location_action">"Compartir mi ubicación"</string>
|
||||
<string name="screen_share_open_apple_maps">"Abrir en Apple Maps"</string>
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@
|
|||
<string name="common_edited_suffix">"(muudetud)"</string>
|
||||
<string name="common_editing">"Muutmine"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption">"Krüptimine"</string>
|
||||
<string name="common_encryption_enabled">"Krüptimine on kasutusel"</string>
|
||||
<string name="common_enter_your_pin">"Sisesta oma PIN-kood"</string>
|
||||
<string name="common_error">"Viga"</string>
|
||||
|
|
@ -158,6 +159,7 @@ Põhjus: %1$s."</string>
|
|||
<string name="common_file">"Fail"</string>
|
||||
<string name="common_file_saved_on_disk_android">"Fail on salvestatud kausta Allalaadimised"</string>
|
||||
<string name="common_forward_message">"Edasta sõnum"</string>
|
||||
<string name="common_frequently_used">"Sagedasti kasutatud"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Pilt"</string>
|
||||
<string name="common_in_reply_to">"Vastuseks kasutajale %1$s"</string>
|
||||
|
|
@ -238,7 +240,9 @@ Põhjus: %1$s."</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>
|
||||
<string name="common_unable_to_decrypt_insecure_device">"Saadetud ebaturvalisest seadmest"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"Sul pole ligipääsu antud sõnumile"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"Saatja verifitseeritud identiteet on muutunud"</string>
|
||||
<string name="common_unable_to_invite_message">"Kutset polnud võimalik saata ühele või enamale kasutajale."</string>
|
||||
<string name="common_unable_to_invite_title">"Kutse(te) saatmine ei õnnestunud"</string>
|
||||
<string name="common_unlock">"Eemalda lukustus"</string>
|
||||
|
|
@ -291,13 +295,9 @@ Põhjus: %1$s."</string>
|
|||
<string name="screen_create_room_access_section_header">"Ligipääs jututoale"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_description">"Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_title">"Küsi võimalust liitumiseks"</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Tühista liitumispalve"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Jah, tühista"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Kas sa oled kindel, et soovid tühistada oma palve jututoaga liitumiseks?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Tühista liitumispalve"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Selgitus (kui soovid lisada)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Kui sinu liitumispalvega ollakse nõus, siis saad kutse jututoaga liitumiseks."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Liitumispalve on saadetud"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Selleks, et see jututuba oleks nähtav jututubade avalikus kataloogis, sa vajad jututoa aadressi."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Jututoa aadress"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Meediafaili üleslaadimine ei õnnestunud. Palun proovi uuesti."</string>
|
||||
|
|
@ -321,20 +321,12 @@ Põhjus: %1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Esiletõstetud sõnumid"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Kasutaja andmete laadimine ei õnnestunud"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blokeeri"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Blokeeritud kasutajad ei saa sulle kirjutada ja kõik nende sõnumid on sinu eest peidetud. Sa saad alati blokeeringu eemaldada."</string>
|
||||
<string name="screen_room_member_details_block_user">"Blokeeri kasutaja"</string>
|
||||
<string name="screen_room_member_details_title">"Profiil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Eemalda blokeering"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Nüüd näed sa jälle kõiki tema sõnumeid"</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Eemalda kasutajalt blokeering"</string>
|
||||
<string name="screen_room_member_details_verify_button_subtitle">"Kasutaja verifitseerimiseks kasuta veebirakendust."</string>
|
||||
<string name="screen_room_member_details_verify_button_title">"Verifitseeri kasutaja %1$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s / %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s esiletõstetud sõnumit"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Laadime sõnumit…"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"Näita kõiki"</string>
|
||||
<string name="screen_room_title">"Vestlus"</string>
|
||||
<string name="screen_roomlist_knock_event_sent_description">"Liitumispäring on saadetud"</string>
|
||||
<string name="screen_share_location_title">"Jaga asukohta"</string>
|
||||
<string name="screen_share_my_location_action">"Jaga minu asukohta"</string>
|
||||
<string name="screen_share_open_apple_maps">"Ava Apple Mapsis"</string>
|
||||
|
|
|
|||
|
|
@ -262,12 +262,6 @@
|
|||
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"فرستادن پیام به هر روی"</string>
|
||||
<string name="screen_room_details_pinned_events_row_title">"پیامهای سنجاق شده"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"بلوک"</string>
|
||||
<string name="screen_room_member_details_block_user">"انسداد کاربر"</string>
|
||||
<string name="screen_room_member_details_title">"نمایه"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"رفع انسداد"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"قادر خواهید بود دوباره همهٔ پیامهایش را ببینید."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"رفع انسداد کاربر"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s از %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s پیامهای سنجاق شده"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"بار کردن پشامها…"</string>
|
||||
|
|
|
|||
|
|
@ -290,10 +290,6 @@ Raison: %1$s."</string>
|
|||
<string name="screen_create_room_access_section_header">"Accès au salon"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_description">"Tout le monde peut demander à rejoindre le salon, mais un administrateur ou un modérateur devra accepter la demande"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_title">"Demander à rejoindre"</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Annuler la demande"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Message (facultatif)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Vous recevrez une invitation à rejoindre le salon si votre demande est acceptée."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Demande de rejoindre le salon envoyée"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Échec de la sélection du média, veuillez réessayer."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Échec du téléchargement du média, veuillez réessayer."</string>
|
||||
|
|
@ -317,15 +313,6 @@ Raison: %1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Messages épinglés"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Impossible de récupérer les détails de l’utilisateur"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Bloquer"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment."</string>
|
||||
<string name="screen_room_member_details_block_user">"Bloquer l’utilisateur"</string>
|
||||
<string name="screen_room_member_details_title">"Profil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Débloquer"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Vous pourrez à nouveau voir tous ses messages."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Débloquer l’utilisateur"</string>
|
||||
<string name="screen_room_member_details_verify_button_subtitle">"Utilisez l’application Web pour vérifier cet utilisateur."</string>
|
||||
<string name="screen_room_member_details_verify_button_title">"Vérifier %1$s"</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>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Chargement du message…"</string>
|
||||
|
|
|
|||
|
|
@ -291,13 +291,6 @@ Ok: %1$s."</string>
|
|||
<string name="screen_create_room_access_section_header">"Szobahozzáférés"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_description">"Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_title">"Csatlakozás kérése"</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Kérés visszavonása"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Igen, visszavonás"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Biztos, hogy visszavonja a szobához való csatlakozási kérését?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Csatlakozási kérés visszavonása"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Üzenet (nem kötelező)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Ha a kérését elfogadják, meghívót kap a szobához való csatlakozáshoz."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Csatlakozási kérés elküldve"</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_media_upload_preview_error_failed_processing">"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nem sikerült a média feltöltése, próbálja újra."</string>
|
||||
|
|
@ -321,15 +314,6 @@ Ok: %1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Kitűzött üzenetek"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nem sikerült letölteni a felhasználói adatokat"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Letiltás"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat."</string>
|
||||
<string name="screen_room_member_details_block_user">"Felhasználó letiltása"</string>
|
||||
<string name="screen_room_member_details_title">"Profil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Letiltás feloldása"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Újra láthatja az összes üzenetét."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Felhasználó kitiltásának feloldása"</string>
|
||||
<string name="screen_room_member_details_verify_button_subtitle">"Használja a webes alkalmazást a felhasználó ellenőrzéséhez."</string>
|
||||
<string name="screen_room_member_details_verify_button_title">"A(z) %1$s ellenőrzése"</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>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Üzenet betöltése…"</string>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<string name="action_back">"Kembali"</string>
|
||||
<string name="action_call">"Panggil"</string>
|
||||
<string name="action_cancel">"Batal"</string>
|
||||
<string name="action_cancel_for_now">"Batalkan untuk saat ini"</string>
|
||||
<string name="action_choose_photo">"Pilih foto"</string>
|
||||
<string name="action_clear">"Hapus"</string>
|
||||
<string name="action_close">"Tutup"</string>
|
||||
|
|
@ -78,6 +79,7 @@
|
|||
<string name="action_ok">"Oke"</string>
|
||||
<string name="action_open_settings">"Pengaturan"</string>
|
||||
<string name="action_open_with">"Buka dengan"</string>
|
||||
<string name="action_pin">"Sematkan"</string>
|
||||
<string name="action_quick_reply">"Balas cepat"</string>
|
||||
<string name="action_quote">"Kutip"</string>
|
||||
<string name="action_react">"Bereaksi"</string>
|
||||
|
|
@ -88,6 +90,7 @@
|
|||
<string name="action_report_bug">"Laporkan kutu"</string>
|
||||
<string name="action_report_content">"Laporkan Konten"</string>
|
||||
<string name="action_reset">"Atur ulang"</string>
|
||||
<string name="action_reset_identity">"Atur ulang identitas"</string>
|
||||
<string name="action_retry">"Coba lagi"</string>
|
||||
<string name="action_retry_decryption">"Coba dekripsi ulang"</string>
|
||||
<string name="action_save">"Simpan"</string>
|
||||
|
|
@ -107,6 +110,8 @@
|
|||
<string name="action_take_photo">"Ambil foto"</string>
|
||||
<string name="action_tap_for_options">"Ketuk untuk opsi"</string>
|
||||
<string name="action_try_again">"Coba lagi"</string>
|
||||
<string name="action_unpin">"Lepaskan sematan"</string>
|
||||
<string name="action_view_in_timeline">"Lihat di lini masa"</string>
|
||||
<string name="action_view_source">"Tampilkan sumber"</string>
|
||||
<string name="action_yes">"Ya"</string>
|
||||
<string name="common_about">"Tentang"</string>
|
||||
|
|
@ -165,11 +170,13 @@ Alasan: %1$s."</string>
|
|||
<string name="common_no_results">"Tidak ada hasil"</string>
|
||||
<string name="common_no_room_name">"Tidak ada nama ruangan"</string>
|
||||
<string name="common_offline">"Luring"</string>
|
||||
<string name="common_open_source_licenses">"Lisensi sumber terbuka"</string>
|
||||
<string name="common_or">"atau"</string>
|
||||
<string name="common_password">"Kata sandi"</string>
|
||||
<string name="common_people">"Orang"</string>
|
||||
<string name="common_permalink">"Tautan Permanen"</string>
|
||||
<string name="common_permission">"Perizinan"</string>
|
||||
<string name="common_pinned">"Disematkan"</string>
|
||||
<string name="common_please_wait">"Mohon tunggu…"</string>
|
||||
<string name="common_poll_end_confirmation">"Apakah Anda yakin ingin mengakhiri pemungutan suara ini?"</string>
|
||||
<string name="common_poll_summary">"Pemungutan suara: %1$s"</string>
|
||||
|
|
@ -251,6 +258,12 @@ Alasan: %1$s."</string>
|
|||
<string name="error_missing_microphone_voice_rationale_android">"%1$s tidak memiliki izin untuk mengakses mikrofon. Aktifkan akses untuk merekam pesan suara."</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Beberapa pesan belum terkirim"</string>
|
||||
<string name="error_unknown">"Maaf, terjadi kesalahan"</string>
|
||||
<string name="event_shield_reason_authenticity_not_guaranteed">"Keaslian pesan terenkripsi ini tidak dapat dijamin pada perangkat ini."</string>
|
||||
<string name="event_shield_reason_previously_verified">"Dienkripsi oleh pengguna yang telah diverifikasi sebelumnya."</string>
|
||||
<string name="event_shield_reason_sent_in_clear">"Tidak dienkripsi."</string>
|
||||
<string name="event_shield_reason_unknown_device">"Dienkripsi oleh perangkat yang tidak dikenal atau dihapus."</string>
|
||||
<string name="event_shield_reason_unsigned_device">"Dienkripsi oleh perangkat yang tidak diverifikasi oleh pemiliknya."</string>
|
||||
<string name="event_shield_reason_unverified_identity">"Dienkripsi oleh pengguna yang tidak terverifikasi."</string>
|
||||
<string name="invite_friends_rich_title">"🔐️ Bergabunglah dengan saya di %1$s"</string>
|
||||
<string name="invite_friends_text">"Hai, bicaralah dengan saya di %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
|
|
@ -258,15 +271,24 @@ Alasan: %1$s."</string>
|
|||
<string name="screen_media_picker_error_failed_selection">"Gagal memilih media, silakan coba lagi."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Gagal memproses media untuk diunggah, silakan coba lagi."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Gagal mengunggah media, silakan coba lagi."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Tekan pesan dan pilih “%1$s” untuk disertakan di sini."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Sematkan pesan penting agar mudah ditemukan"</string>
|
||||
<plurals name="screen_pinned_timeline_screen_title">
|
||||
<item quantity="other">"%1$d Pesan yang disematkan"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Pesan yang disematkan"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"Anda akan pergi ke akun %1$s Anda untuk mengatur ulang identitas Anda. Setelah itu Anda akan dibawa kembali ke aplikasi."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Tidak dapat mengonfirmasi? Buka akun Anda untuk mengatur ulang identitas Anda."</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_primary_button_title">"Kirim pesan saja"</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_subtitle">"%1$s menggunakan satu atau beberapa perangkat yang belum diverifikasi. Anda tetap dapat mengirim pesan, atau Anda dapat membatalkan untuk saat ini dan mencoba lagi nanti setelah %2$s telah memverifikasi semua perangkat mereka."</string>
|
||||
<string name="screen_resolve_send_failure_unsigned_device_title">"Pesan Anda tidak terkirim karena %1$s belum memverifikasi semua perangkat"</string>
|
||||
<string name="screen_room_details_pinned_events_row_title">"Pesan yang disematkan"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Gagal memproses media untuk diunggah, silakan coba lagi."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Tidak dapat mengambil detail pengguna"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blokir"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Pengguna yang diblokir tidak akan dapat mengirim Anda pesan dan semua pesan mereka akan disembunyikan. Anda dapat membuka blokirnya kapan saja."</string>
|
||||
<string name="screen_room_member_details_block_user">"Blokir pengguna"</string>
|
||||
<string name="screen_room_member_details_title">"Profil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Buka blokir"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Anda akan dapat melihat semua pesan dari mereka lagi."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Buka blokir pengguna"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s dari %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Pesan yang disematkan"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Memuat pesan…"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"Lihat Semua"</string>
|
||||
<string name="screen_room_title">"Obrolan"</string>
|
||||
<string name="screen_share_location_title">"Bagikan lokasi"</string>
|
||||
<string name="screen_share_my_location_action">"Bagikan lokasi saya"</string>
|
||||
|
|
@ -274,6 +296,8 @@ Alasan: %1$s."</string>
|
|||
<string name="screen_share_open_google_maps">"Buka di Google Maps"</string>
|
||||
<string name="screen_share_open_osm_maps">"Buka di OpenStreetMap"</string>
|
||||
<string name="screen_share_this_location_action">"Bagikan lokasi ini"</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Pesan tidak terkirim karena identitas terverifikasi %1$s telah berubah."</string>
|
||||
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Pesan tidak terkirim karena %1$s belum memverifikasi semua perangkat."</string>
|
||||
<string name="screen_view_location_title">"Lokasi"</string>
|
||||
<string name="settings_version_number">"Versi: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"id"</string>
|
||||
|
|
|
|||
|
|
@ -302,13 +302,6 @@ Motivo:. %1$s"</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Messaggi fissati"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Elaborazione del file multimediale da caricare fallita, riprova."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Impossibile recuperare i dettagli dell\'utente"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blocca"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti quelli già ricevuti saranno nascosti. Puoi sbloccarli in qualsiasi momento."</string>
|
||||
<string name="screen_room_member_details_block_user">"Blocca utente"</string>
|
||||
<string name="screen_room_member_details_title">"Profilo"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Sblocca"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Potrai vedere di nuovo tutti i suoi messaggi."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Sblocca utente"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s di %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Messaggi fissati"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Caricamento messaggio…"</string>
|
||||
|
|
|
|||
|
|
@ -238,12 +238,6 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"მედიის ატვირთვა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"მომხმარებლის მონაცემების მოძიება ვერ მოხერხდა"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"დაბლოკვა"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"დაბლოკილი მომხმარებლები ვერ შეძლებენ თქვენთვის შეტყობინების გაგზავნას და ყველა მათი შეტყობინება თქვენთვის დამალული იქნება. თქვენ მათი განბლოკვა ნებისმეირ დროს შეგიძლიათ."</string>
|
||||
<string name="screen_room_member_details_block_user">"მომხმარებლის დაბლოკვა"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"განბლოკვა"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"თქვენ კვლავ შეძლებთ მათგან ყველა შეტყობინების ნახვას."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Მომხმარებლის განბლოკვა"</string>
|
||||
<string name="screen_share_location_title">"მდებარეობის გაზიარება"</string>
|
||||
<string name="screen_share_my_location_action">"ჩემი მდებარეობის გაზიარება"</string>
|
||||
<string name="screen_share_open_apple_maps">"Apple Maps-ში გახსნა"</string>
|
||||
|
|
|
|||
|
|
@ -260,13 +260,6 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"Het uploaden van media is mislukt. Probeer het opnieuw."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Kon gebruikersgegevens niet ophalen"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blokkeren"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Geblokkeerde gebruikers kunnen je geen berichten sturen en al hun berichten worden verborgen. Je kunt ze op elk moment deblokkeren."</string>
|
||||
<string name="screen_room_member_details_block_user">"Gebruiker blokkeren"</string>
|
||||
<string name="screen_room_member_details_title">"Profiel"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Deblokkeren"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Je zult alle berichten van hen weer kunnen zien."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Gebruiker deblokkeren"</string>
|
||||
<string name="screen_room_title">"Chat"</string>
|
||||
<string name="screen_share_location_title">"Locatie delen"</string>
|
||||
<string name="screen_share_my_location_action">"Deel mijn locatie"</string>
|
||||
|
|
|
|||
|
|
@ -307,13 +307,6 @@ Powód: %1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Przypięte wiadomości"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nie można pobrać danych użytkownika"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Zablokuj"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Zablokowani użytkownicy nie będą mogli wysyłać Ci wiadomości, a wszystkie ich wiadomości zostaną ukryte. Możesz odblokować ich w dowolnym momencie."</string>
|
||||
<string name="screen_room_member_details_block_user">"Zablokuj użytkownika"</string>
|
||||
<string name="screen_room_member_details_title">"Profil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Odblokuj"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Będziesz mógł ponownie zobaczyć wszystkie wiadomości od tego użytkownika."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Odblokuj użytkownika"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s z %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s przypiętych wiadomości"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Wczytywanie wiadomości…"</string>
|
||||
|
|
|
|||
|
|
@ -266,12 +266,6 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"Falha ao enviar mídia. Tente novamente."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Falha ao processar mídia para upload. Tente novamente."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Não foi possível recuperar os detalhes do usuário"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Bloquear"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Usuários bloqueados não poderão enviar mensagens para você e todas as mensagens deles serão ocultadas. Você pode desbloqueá-los a qualquer momento."</string>
|
||||
<string name="screen_room_member_details_block_user">"Bloquear usuário"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Desbloquear"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Você poderá ver todas as mensagens deles novamente."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Desbloquear usuário"</string>
|
||||
<string name="screen_share_location_title">"Compartilhar localização"</string>
|
||||
<string name="screen_share_my_location_action">"Compartilhar minha localização"</string>
|
||||
<string name="screen_share_open_apple_maps">"Abrir no Apple Maps"</string>
|
||||
|
|
|
|||
|
|
@ -290,10 +290,6 @@ Razão: %1$s."</string>
|
|||
<string name="screen_create_room_access_section_header">"Acesso à sala"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_description">"Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou um moderador terá de aceitar o pedido"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_title">"Pedir para participar"</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Cancelar pedido"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Mensagem (opcional)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Irá receber um convite para participar na sala se seu pedido for aceite."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Pedido de adesão enviado"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Falha ao selecionar multimédia, por favor tente novamente."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Falha ao processar multimédia para carregamento, por favor tente novamente."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Falhar ao carregar multimédia, por favor tente novamente."</string>
|
||||
|
|
@ -317,15 +313,6 @@ Razão: %1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Mensagens afixadas"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Falha ao processar multimédia para carregamento, por favor tente novamente."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Não foi possível obter os detalhes de utilizador."</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Bloquear"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Os utilizadores bloqueados não poderão enviar-te mensagens e todas as suas mensagens ficarão ocultas. Podes desbloqueá-los em qualquer altura."</string>
|
||||
<string name="screen_room_member_details_block_user">"Bloquear utilizador"</string>
|
||||
<string name="screen_room_member_details_title">"Perfil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Desbloquear"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Poderás voltar a ver todas as suas mensagens."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Desbloquear utilizador"</string>
|
||||
<string name="screen_room_member_details_verify_button_subtitle">"Utiliza a aplicação Web para verificar este utilizador."</string>
|
||||
<string name="screen_room_member_details_verify_button_title">"Verifique %1$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s de %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s mensagens afixadas"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"A carregar mensagem…"</string>
|
||||
|
|
|
|||
|
|
@ -264,13 +264,6 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"Încărcarea fișierelor media a eșuat, încercați din nou."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Procesarea datelor media a eșuat, vă rugăm să încercați din nou."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nu am putut găsi detaliile utilizatorului"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blocați"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
|
||||
<string name="screen_room_member_details_block_user">"Blocați utilizatorul"</string>
|
||||
<string name="screen_room_member_details_title">"Profil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Deblocați"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Deblocați utilizatorul"</string>
|
||||
<string name="screen_room_title">"Chat"</string>
|
||||
<string name="screen_share_location_title">"Partajați locația"</string>
|
||||
<string name="screen_share_my_location_action">"Distribuiți locația mea"</string>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
<string name="action_choose_photo">"Выбрать фото"</string>
|
||||
<string name="action_clear">"Очистить"</string>
|
||||
<string name="action_close">"Закрыть"</string>
|
||||
<string name="action_complete_verification">"Полная проверка"</string>
|
||||
<string name="action_complete_verification">"Завершите подтверждение"</string>
|
||||
<string name="action_confirm">"Подтвердить"</string>
|
||||
<string name="action_confirm_password">"Подтвердите пароль"</string>
|
||||
<string name="action_continue">"Продолжить"</string>
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
<string name="action_copy_link_to_message">"Скопировать ссылку в сообщение"</string>
|
||||
<string name="action_create">"Создать"</string>
|
||||
<string name="action_create_a_room">"Создать комнату"</string>
|
||||
<string name="action_deactivate">"Деактивировать"</string>
|
||||
<string name="action_deactivate">"Отключить"</string>
|
||||
<string name="action_deactivate_account">"Отключить учётную запись"</string>
|
||||
<string name="action_decline">"Отклонить"</string>
|
||||
<string name="action_delete_poll">"Удалить опрос"</string>
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
<string name="action_leave_conversation">"Покинуть беседу"</string>
|
||||
<string name="action_leave_room">"Покинуть комнату"</string>
|
||||
<string name="action_load_more">"Загрузить еще"</string>
|
||||
<string name="action_manage_account">"Настройки аккаунта"</string>
|
||||
<string name="action_manage_account">"Настройки учетной записи"</string>
|
||||
<string name="action_manage_devices">"Управление устройствами"</string>
|
||||
<string name="action_message">"Сообщение"</string>
|
||||
<string name="action_next">"Далее"</string>
|
||||
|
|
@ -147,6 +147,7 @@
|
|||
<string name="common_edited_suffix">"(изменено)"</string>
|
||||
<string name="common_editing">"Редактирование"</string>
|
||||
<string name="common_emote">"%1$s%2$s"</string>
|
||||
<string name="common_encryption">"Шифрование"</string>
|
||||
<string name="common_encryption_enabled">"Шифрование включено"</string>
|
||||
<string name="common_enter_your_pin">"Введите свой PIN-код"</string>
|
||||
<string name="common_error">"Ошибка"</string>
|
||||
|
|
@ -160,6 +161,7 @@
|
|||
<string name="common_file">"Файл"</string>
|
||||
<string name="common_file_saved_on_disk_android">"Файл сохранен в «Загрузки»"</string>
|
||||
<string name="common_forward_message">"Переслать сообщение"</string>
|
||||
<string name="common_frequently_used">"Часто используемые"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Изображения"</string>
|
||||
<string name="common_in_reply_to">"В ответ на %1$s"</string>
|
||||
|
|
@ -186,7 +188,7 @@
|
|||
<string name="common_open_source_licenses">"Лицензии с открытым исходным кодом"</string>
|
||||
<string name="common_or">"или"</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>
|
||||
|
|
@ -203,9 +205,7 @@
|
|||
<string name="common_privacy_policy">"Политика конфиденциальности"</string>
|
||||
<string name="common_reaction">"Реакция"</string>
|
||||
<string name="common_reactions">"Реакции"</string>
|
||||
<string name="common_recovery_key">
|
||||
<b>"Ключ восстановления"</b>
|
||||
</string>
|
||||
<string name="common_recovery_key">"Ключ восстановления"</string>
|
||||
<string name="common_refreshing">"Обновление…"</string>
|
||||
<string name="common_replying_to">"Отвечает на %1$s"</string>
|
||||
<string name="common_report_a_bug">"Сообщить об ошибке"</string>
|
||||
|
|
@ -244,15 +244,17 @@
|
|||
<string name="common_topic">"Тема"</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_invite_message">"Не удалось отправить приглашения одному или нескольким пользователям."</string>
|
||||
<string name="common_unable_to_invite_title">"Не удалось отправить приглашение(я)"</string>
|
||||
<string name="common_unlock">"Разблокировать"</string>
|
||||
<string name="common_unmute">"Вкл. звук"</string>
|
||||
<string name="common_unsupported_event">"Неподдерживаемое событие"</string>
|
||||
<string name="common_username">"Имя пользователя"</string>
|
||||
<string name="common_verification_cancelled">"Проверка отменена"</string>
|
||||
<string name="common_verification_complete">"Проверка завершена"</string>
|
||||
<string name="common_verification_cancelled">"Подтверждение отменено"</string>
|
||||
<string name="common_verification_complete">"Подтверждение завершено"</string>
|
||||
<string name="common_verification_failed">"Сбой проверки"</string>
|
||||
<string name="common_verified">"Проверено"</string>
|
||||
<string name="common_verify_device">"Подтверждение устройства"</string>
|
||||
|
|
@ -297,13 +299,9 @@
|
|||
<string name="screen_create_room_access_section_header">"Доступ в комнату"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_description">"Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_title">"Попросить присоединиться"</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Отменить запрос"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Да, отменить"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Вы действительно хотите отменить заявку на вступление в эту комнату?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Отменить запрос на присоединение"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Сообщение (опционально)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Запрос на присоединение отправлен"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Чтобы эта комната была видна в каталоге общедоступных, вам необходим ее адрес"</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Адрес комнаты"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Видимость комнаты"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать носитель, попробуйте еще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не удалось загрузить медиафайлы, попробуйте еще раз."</string>
|
||||
|
|
@ -317,7 +315,7 @@
|
|||
<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_resolve_send_failure_changed_identity_primary_button_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_unsigned_device_primary_button_title">"Отправь сообщение в любом случае"</string>
|
||||
|
|
@ -328,20 +326,12 @@
|
|||
<string name="screen_room_details_pinned_events_row_title">"Закрепленные сообщения"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не удалось получить данные о пользователе"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Заблокировать"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."</string>
|
||||
<string name="screen_room_member_details_block_user">"Заблокировать пользователя"</string>
|
||||
<string name="screen_room_member_details_title">"Профиль"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Разблокировать"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Вы снова сможете увидеть все сообщения."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Разблокировать пользователя"</string>
|
||||
<string name="screen_room_member_details_verify_button_subtitle">"Используйте веб-приложение для проверки этого пользователя."</string>
|
||||
<string name="screen_room_member_details_verify_button_title">"Верифицировать %1$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s из %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Закрепленные сообщения"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Загрузка сообщения…"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"Посмотреть все"</string>
|
||||
<string name="screen_room_title">"Чат"</string>
|
||||
<string name="screen_roomlist_knock_event_sent_description">"Запрос на присоединение отправлен"</string>
|
||||
<string name="screen_share_location_title">"Поделиться местоположением"</string>
|
||||
<string name="screen_share_my_location_action">"Поделиться моим местоположением"</string>
|
||||
<string name="screen_share_open_apple_maps">"Открыть в Apple Maps"</string>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
<string name="action_forgot_password">"Zabudnuté heslo?"</string>
|
||||
<string name="action_forward">"Preposlať"</string>
|
||||
<string name="action_go_back">"Ísť späť"</string>
|
||||
<string name="action_ignore">"Ignorovať"</string>
|
||||
<string name="action_invite">"Pozvať"</string>
|
||||
<string name="action_invite_friends">"Pozvať ľudí"</string>
|
||||
<string name="action_invite_friends_to_app">"Pozvať ľudí do %1$s"</string>
|
||||
|
|
@ -140,11 +141,13 @@
|
|||
<string name="common_dark">"Tmavý"</string>
|
||||
<string name="common_decryption_error">"Chyba dešifrovania"</string>
|
||||
<string name="common_developer_options">"Možnosti pre vývojárov"</string>
|
||||
<string name="common_device_id">"ID zariadenia"</string>
|
||||
<string name="common_direct_chat">"Priama konverzácia"</string>
|
||||
<string name="common_do_not_show_this_again">"Nezobrazovať toto znova"</string>
|
||||
<string name="common_edited_suffix">"(upravené)"</string>
|
||||
<string name="common_editing">"Upravuje sa"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption">"Šifrovanie"</string>
|
||||
<string name="common_encryption_enabled">"Šifrovanie zapnuté"</string>
|
||||
<string name="common_enter_your_pin">"Zadajte svoj PIN"</string>
|
||||
<string name="common_error">"Chyba"</string>
|
||||
|
|
@ -240,7 +243,9 @@ Dôvod: %1$s."</string>
|
|||
<string name="common_topic">"Téma"</string>
|
||||
<string name="common_topic_placeholder">"O čom je táto miestnosť?"</string>
|
||||
<string name="common_unable_to_decrypt">"Nie je možné dešifrovať"</string>
|
||||
<string name="common_unable_to_decrypt_insecure_device">"Odoslané z nezabezpečeného zariadenia"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"Nemáte prístup k tejto správe"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"Overená totožnosť odosielateľa sa zmenila"</string>
|
||||
<string name="common_unable_to_invite_message">"Pozvánky nebolo možné odoslať jednému alebo viacerým používateľom."</string>
|
||||
<string name="common_unable_to_invite_title">"Nie je možné odoslať pozvánku/ky"</string>
|
||||
<string name="common_unlock">"Odomknúť"</string>
|
||||
|
|
@ -249,6 +254,8 @@ Dôvod: %1$s."</string>
|
|||
<string name="common_username">"Používateľské meno"</string>
|
||||
<string name="common_verification_cancelled">"Overovanie zrušené"</string>
|
||||
<string name="common_verification_complete">"Overovanie je dokončené"</string>
|
||||
<string name="common_verification_failed">"Overenie zlyhalo"</string>
|
||||
<string name="common_verified">"Overené"</string>
|
||||
<string name="common_verify_device">"Overiť zariadenie"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_voice_message">"Hlasová správa"</string>
|
||||
|
|
@ -256,6 +263,8 @@ Dôvod: %1$s."</string>
|
|||
<string name="common_waiting_for_decryption_key">"Čaká sa na dešifrovací kľúč"</string>
|
||||
<string name="common_you">"Vy"</string>
|
||||
<string name="crypto_identity_change_pin_violation">"Zdá sa, že totožnosť používateľa %1$s sa zmenila.%2$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new">"Zdá sa, že identita %2$s používateľa %1$s sa zmenila. %3$s"</string>
|
||||
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
|
||||
<string name="dialog_title_confirmation">"Potvrdenie"</string>
|
||||
<string name="dialog_title_error">"Chyba"</string>
|
||||
<string name="dialog_title_success">"Úspech"</string>
|
||||
|
|
@ -284,6 +293,14 @@ Dôvod: %1$s."</string>
|
|||
<string name="invite_friends_text">"Ahoj, porozprávajte sa so mnou na %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Zúrivo potriasť pre nahlásenie chyby"</string>
|
||||
<string name="screen_create_room_access_section_anyone_option_description">"Do tejto miestnosti sa môže pripojiť ktokoľvek"</string>
|
||||
<string name="screen_create_room_access_section_anyone_option_title">"Ktokoľvek"</string>
|
||||
<string name="screen_create_room_access_section_header">"Prístup do miestnosti"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_description">"Ktokoľvek môže požiadať o pripojenie sa k miestnosti, ale administrátor alebo moderátor bude musieť žiadosť schváliť"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_title">"Požiadať o pripojenie"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"Aby bola táto miestnosť viditeľná v adresári verejných miestností, budete potrebovať adresu miestnosti."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Adresa miestnosti"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Viditeľnosť miestnosti"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Nepodarilo sa vybrať médium, skúste to prosím znova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nepodarilo sa nahrať médiá, skúste to prosím znova."</string>
|
||||
|
|
@ -308,18 +325,12 @@ Dôvod: %1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Pripnuté správy"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nepodarilo sa získať údaje o používateľovi"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Zablokovať"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať."</string>
|
||||
<string name="screen_room_member_details_block_user">"Zablokovať používateľa"</string>
|
||||
<string name="screen_room_member_details_title">"Profil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Odblokovať"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Všetky správy od nich budete môcť opäť vidieť."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Odblokovať používateľa"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s z %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Pripnutých správ"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Načítava sa správa…"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"Zobraziť všetko"</string>
|
||||
<string name="screen_room_title">"Konverzácia"</string>
|
||||
<string name="screen_roomlist_knock_event_sent_description">"Žiadosť o vstup odoslaná"</string>
|
||||
<string name="screen_share_location_title">"Zdieľať polohu"</string>
|
||||
<string name="screen_share_my_location_action">"Zdieľať moju polohu"</string>
|
||||
<string name="screen_share_open_apple_maps">"Otvoriť v Apple Maps"</string>
|
||||
|
|
|
|||
|
|
@ -286,13 +286,6 @@ Anledning:%1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Fästa meddelanden"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Kunde inte hämta användarinformation"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blockera"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Blockerade användare kommer inte att kunna skicka meddelanden till dig och alla deras meddelanden kommer att döljas. Du kan avblockera dem när som helst."</string>
|
||||
<string name="screen_room_member_details_block_user">"Blockera användare"</string>
|
||||
<string name="screen_room_member_details_title">"Profil"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Avblockera"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Du kommer att kunna se alla meddelanden från dem igen."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Avblockera användare"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s av %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Fästa meddelanden"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Laddar meddelande …"</string>
|
||||
|
|
|
|||
|
|
@ -276,13 +276,6 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"Не вдалося завантажити медіафайл, спробуйте ще раз."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не вдалося отримати дані користувача"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Заблокувати"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Заблоковані користувачі не зможуть надсилати Вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час."</string>
|
||||
<string name="screen_room_member_details_block_user">"Заблокувати користувача"</string>
|
||||
<string name="screen_room_member_details_title">"Профіль"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Розблокувати"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Ви знову зможете бачити всі повідомлення від них."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Розблокувати користувача"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s із %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Закріплених повідомлень"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"Переглянути всі"</string>
|
||||
|
|
|
|||
|
|
@ -192,12 +192,6 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"Media yuklanmadi, qayta urinib ko‘ring."</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Mediani yuklab bo‘lmadi, qayta urinib ko‘ring."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Foydalanuvchi tafsilotlarini olinmadi"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Bloklash"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Bloklangan foydalanuvchilar sizga xabar yubora olmaydi va ularning barcha xabarlari yashiriladi. Ularni istalgan vaqtda blokdan chiqarishingiz mumkin."</string>
|
||||
<string name="screen_room_member_details_block_user">"Foydalanuvchini bloklash"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Blokdan chiqarish"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"Ulardan kelgan barcha xabarlarni yana koʻrishingiz mumkin boʻladi."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Foydalanuvchini blokdan chiqarish"</string>
|
||||
<string name="screen_share_location_title">"Joylashuvni ulashish"</string>
|
||||
<string name="screen_share_my_location_action">"Joylashuvimni ulashing"</string>
|
||||
<string name="screen_share_open_apple_maps">"Apple Mapsda oching"</string>
|
||||
|
|
|
|||
|
|
@ -242,13 +242,6 @@
|
|||
<string name="invite_friends_text">"嘿,來 %1$s 和我聊天:%2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"無法上傳媒體檔案,請稍後再試。"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"封鎖"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"被封鎖的使用者無法傳訊息給您,他們的訊息會被隱藏。您可以在任何時候解除封鎖。"</string>
|
||||
<string name="screen_room_member_details_block_user">"封鎖使用者"</string>
|
||||
<string name="screen_room_member_details_title">"個人檔案"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"解除封鎖"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"您將無法看到任何來自他們的訊息。"</string>
|
||||
<string name="screen_room_member_details_unblock_user">"解除封鎖使用者"</string>
|
||||
<string name="screen_share_location_title">"分享位置"</string>
|
||||
<string name="screen_share_my_location_action">"分享我的位置"</string>
|
||||
<string name="screen_share_open_apple_maps">"在 Apple Maps 中開啟"</string>
|
||||
|
|
|
|||
|
|
@ -281,13 +281,6 @@
|
|||
<string name="screen_room_details_pinned_events_row_title">"置顶消息"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"处理要上传的媒体失败,请重试。"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"无法获取用户信息"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"封禁"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"被封禁的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解封。"</string>
|
||||
<string name="screen_room_member_details_block_user">"封禁用户"</string>
|
||||
<string name="screen_room_member_details_title">"个人资料"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"解封"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"可以重新接收他们的消息。"</string>
|
||||
<string name="screen_room_member_details_unblock_user">"解封用户"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s / %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"置顶消息 %1$s"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"正在加载消息…"</string>
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@
|
|||
<string name="common_edited_suffix">"(edited)"</string>
|
||||
<string name="common_editing">"Editing"</string>
|
||||
<string name="common_emote">"* %1$s %2$s"</string>
|
||||
<string name="common_encryption">"Encryption"</string>
|
||||
<string name="common_encryption_enabled">"Encryption enabled"</string>
|
||||
<string name="common_enter_your_pin">"Enter your PIN"</string>
|
||||
<string name="common_error">"Error"</string>
|
||||
|
|
@ -158,6 +159,7 @@ Reason: %1$s."</string>
|
|||
<string name="common_file">"File"</string>
|
||||
<string name="common_file_saved_on_disk_android">"File saved to Downloads"</string>
|
||||
<string name="common_forward_message">"Forward message"</string>
|
||||
<string name="common_frequently_used">"Frequently used"</string>
|
||||
<string name="common_gif">"GIF"</string>
|
||||
<string name="common_image">"Image"</string>
|
||||
<string name="common_in_reply_to">"In reply to %1$s"</string>
|
||||
|
|
@ -238,7 +240,9 @@ Reason: %1$s."</string>
|
|||
<string name="common_topic">"Topic"</string>
|
||||
<string name="common_topic_placeholder">"What is this room about?"</string>
|
||||
<string name="common_unable_to_decrypt">"Unable to decrypt"</string>
|
||||
<string name="common_unable_to_decrypt_insecure_device">"Sent from an insecure device"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"You don\'t have access to this message"</string>
|
||||
<string name="common_unable_to_decrypt_verification_violation">"Sender\'s verified identity has changed"</string>
|
||||
<string name="common_unable_to_invite_message">"Invites couldn\'t be sent to one or more users."</string>
|
||||
<string name="common_unable_to_invite_title">"Unable to send invite(s)"</string>
|
||||
<string name="common_unlock">"Unlock"</string>
|
||||
|
|
@ -250,6 +254,7 @@ Reason: %1$s."</string>
|
|||
<string name="common_verification_failed">"Verification failed"</string>
|
||||
<string name="common_verified">"Verified"</string>
|
||||
<string name="common_verify_device">"Verify device"</string>
|
||||
<string name="common_verify_identity">"Verify identity"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_voice_message">"Voice message"</string>
|
||||
<string name="common_waiting">"Waiting…"</string>
|
||||
|
|
@ -291,13 +296,9 @@ Reason: %1$s."</string>
|
|||
<string name="screen_create_room_access_section_header">"Room Access"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_description">"Anyone can ask to join the room but an administrator or a moderator will have to accept the request"</string>
|
||||
<string name="screen_create_room_access_section_knocking_option_title">"Ask to join"</string>
|
||||
<string name="screen_join_room_cancel_knock_action">"Cancel request"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_confirmation">"Yes, cancel"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_description">"Are you sure that you want to cancel your request to join this room?"</string>
|
||||
<string name="screen_join_room_cancel_knock_alert_title">"Cancel request to join"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Message (optional)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"You will receive an invite to join the room if your request is accepted."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Request to join sent"</string>
|
||||
<string name="screen_create_room_room_address_section_footer">"In order for this room to be visible in the public room directory, you will need a room address."</string>
|
||||
<string name="screen_create_room_room_address_section_title">"Room address"</string>
|
||||
<string name="screen_create_room_room_visibility_section_title">"Room visibility"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
||||
|
|
@ -321,20 +322,12 @@ Reason: %1$s."</string>
|
|||
<string name="screen_room_details_pinned_events_row_title">"Pinned messages"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Block"</string>
|
||||
<string name="screen_room_member_details_block_alert_description">"Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime."</string>
|
||||
<string name="screen_room_member_details_block_user">"Block user"</string>
|
||||
<string name="screen_room_member_details_title">"Profile"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_action">"Unblock"</string>
|
||||
<string name="screen_room_member_details_unblock_alert_description">"You\'ll be able to see all messages from them again."</string>
|
||||
<string name="screen_room_member_details_unblock_user">"Unblock user"</string>
|
||||
<string name="screen_room_member_details_verify_button_subtitle">"Use the web app to verify this user."</string>
|
||||
<string name="screen_room_member_details_verify_button_title">"Verify %1$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator">"%1$s of %2$s"</string>
|
||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Pinned messages"</string>
|
||||
<string name="screen_room_pinned_banner_loading_description">"Loading message…"</string>
|
||||
<string name="screen_room_pinned_banner_view_all_button_title">"View All"</string>
|
||||
<string name="screen_room_title">"Chat"</string>
|
||||
<string name="screen_roomlist_knock_event_sent_description">"Request to join sent"</string>
|
||||
<string name="screen_share_location_title">"Share location"</string>
|
||||
<string name="screen_share_my_location_action">"Share my location"</string>
|
||||
<string name="screen_share_open_apple_maps">"Open in Apple Maps"</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue