Merge branch 'develop' into renovate/kotlin

This commit is contained in:
Benoit Marty 2024-10-30 11:14:29 +01:00 committed by GitHub
commit 5bc7b897cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1157 changed files with 7158 additions and 4277 deletions

View file

@ -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
}

View file

@ -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,
)
)
}

View file

@ -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,
}

View file

@ -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",
)

View file

@ -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)
}
}
}

View file

@ -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,
)
}
}

View file

@ -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
}

View file

@ -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,
)

View file

@ -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,

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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,

View file

@ -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>

View file

@ -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

View file

@ -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
}

View file

@ -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.
*/

View file

@ -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>
}

View file

@ -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

View file

@ -9,5 +9,8 @@ package io.element.android.libraries.matrix.api.timeline.item.event
enum class UtdCause {
Unknown,
Membership,
SentBeforeWeJoined,
VerificationViolation,
UnsignedDevice,
UnknownDevice
}

View file

@ -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

View file

@ -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
}

View file

@ -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) {

View file

@ -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,

View file

@ -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")
}
}

View file

@ -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) {

View file

@ -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()
}
}

View file

@ -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,
)
}

View file

@ -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()
}
}

View file

@ -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
)
}
}
}

View file

@ -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(),
)
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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(),
)

View file

@ -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
)
)
}
}

View file

@ -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,

View file

@ -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,
)
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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,
)
)
}

View file

@ -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

View file

@ -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"
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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,

View file

@ -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)

View file

@ -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

View file

@ -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,
)
}

View file

@ -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()

View file

@ -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)

View file

@ -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,

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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)

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 &amp; 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 lutilisateur"</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 lutilisateur"</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 lutilisateur"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Utilisez lapplication 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -192,12 +192,6 @@
<string name="screen_media_upload_preview_error_failed_sending">"Media yuklanmadi, qayta urinib koring."</string>
<string name="screen_room_error_failed_processing_media">"Mediani yuklab bolmadi, qayta urinib koring."</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>

View file

@ -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>

View file

@ -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>

View file

@ -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>