Merge branch 'develop' into feature/fga/mark_room_as_favorite

This commit is contained in:
ganfra 2024-02-12 17:08:36 +01:00
commit a8bc0cb4ca
538 changed files with 4465 additions and 1639 deletions

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_compatible_app_found">"Не знойдзена сумяшчальная праграма для выканання гэтага дзеяння."</string>
</resources>

View file

@ -18,6 +18,8 @@ package io.element.android.libraries.dateformatter.test
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
const val A_FORMATTED_DATE = "formatted_date"
class FakeLastMessageTimestampFormatter : LastMessageTimestampFormatter {
private var format = ""
fun givenFormat(format: String) {

View file

@ -68,7 +68,9 @@ fun HeaderFooterPage(
content()
}
// Footer
footer()
Box(modifier = Modifier.padding(horizontal = 16.dp)) {
footer()
}
}
}
}

View file

@ -67,13 +67,12 @@ fun OnBoardingPage(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.padding(vertical = 16.dp),
.padding(all = 20.dp),
) {
// Content
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 24.dp)
.fillMaxWidth(),
horizontalAlignment = contentAlignment,
) {

View file

@ -37,8 +37,10 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.annotations.CoreColorToken
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.compound.tokens.generated.internal.DarkColorTokens
import io.element.android.compound.tokens.generated.internal.LightColorTokens
import io.element.android.libraries.designsystem.R
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -54,7 +56,11 @@ fun SunsetPage(
modifier: Modifier = Modifier,
overallContent: @Composable () -> Unit,
) {
ForcedDarkElementTheme(lightStatusBar = true) {
ElementTheme(
// Always use the opposite value of the current theme
darkTheme = ElementTheme.isLightTheme,
applySystemBarsUpdate = false,
) {
Box(
modifier = modifier.fillMaxSize()
) {
@ -107,21 +113,32 @@ fun SunsetPage(
}
}
@OptIn(CoreColorToken::class)
@Composable
private fun SunsetBackground(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxSize()) {
private fun SunsetBackground() {
Column(modifier = Modifier.fillMaxSize()) {
// The top background colors are the opposite of the current theme ones
val topBackgroundColor = if (ElementTheme.isLightTheme) {
DarkColorTokens.colorThemeBg
} else {
LightColorTokens.colorThemeBg
}
// The bottom background colors follow the current theme
val bottomBackgroundColor = if (ElementTheme.isLightTheme) {
LightColorTokens.colorThemeBg
} else {
// The dark background color doesn't 100% match the image, so we use a custom color
Color(0xFF121418)
}
Box(
modifier = Modifier
.fillMaxWidth()
.weight(0.3f)
.background(Color.White)
.background(topBackgroundColor)
)
Image(
modifier = Modifier
.fillMaxWidth(),
painter = painterResource(id = R.drawable.light_dark),
modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.drawable.bg_migration),
contentScale = ContentScale.Crop,
contentDescription = null,
)
@ -129,7 +146,7 @@ private fun SunsetBackground(
modifier = Modifier
.fillMaxWidth()
.weight(0.7f)
.background(Color(0xFF121418))
.background(bottomBackgroundColor)
)
}
}

View file

@ -23,7 +23,6 @@ import android.os.Build
import android.text.TextPaint
import androidx.annotation.FloatRange
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
@ -136,13 +135,13 @@ object BloomDefaults {
@Composable
fun defaultLayers() = persistentListOf(
// Bottom layer
if (isSystemInDarkTheme()) {
BloomLayer(0.5f, BlendMode.Exclusion)
} else {
if (ElementTheme.isLightTheme) {
BloomLayer(0.2f, BlendMode.Hardlight)
} else {
BloomLayer(0.5f, BlendMode.Exclusion)
},
// Top layer
BloomLayer(if (isSystemInDarkTheme()) 0.2f else 0.8f, BlendMode.Color),
BloomLayer(if (ElementTheme.isLightTheme) 0.8f else 0.2f, BlendMode.Color),
)
}

View file

@ -73,7 +73,6 @@ fun BlurHashAsyncImage(
@Composable
private fun BlurHashImage(
blurHash: String?,
modifier: Modifier = Modifier,
contentDescription: String? = null,
contentScale: ContentScale = ContentScale.Fit,
) {
@ -91,7 +90,7 @@ private fun BlurHashImage(
}
bitmapState.value?.let { bitmap ->
Image(
modifier = modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
bitmap = bitmap.asImageBitmap(),
contentScale = contentScale,
contentDescription = contentDescription

View file

@ -65,7 +65,6 @@ private fun ConfirmationDialogContent(
cancelText: String,
onSubmitClicked: () -> Unit,
onCancelClicked: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
thirdButtonText: String? = null,
onThirdButtonClicked: () -> Unit = {},
@ -73,7 +72,6 @@ private fun ConfirmationDialogContent(
icon: @Composable (() -> Unit)? = null,
) {
SimpleAlertDialogContent(
modifier = modifier,
title = title,
content = content,
submitText = submitText,

View file

@ -51,12 +51,10 @@ fun ErrorDialog(
private fun ErrorDialogContent(
content: String,
onSubmitClicked: () -> Unit,
modifier: Modifier = Modifier,
title: String = ErrorDialogDefaults.title,
submitText: String = ErrorDialogDefaults.submitText,
) {
SimpleAlertDialogContent(
modifier = modifier,
title = title,
content = content,
submitText = submitText,

View file

@ -80,7 +80,6 @@ private fun ListDialogContent(
onSubmitClicked: () -> Unit,
cancelText: String,
submitText: String,
modifier: Modifier = Modifier,
title: String? = null,
enabled: Boolean = true,
subtitle: @Composable (() -> Unit)? = null,
@ -88,7 +87,6 @@ private fun ListDialogContent(
SimpleAlertDialogContent(
title = title,
subtitle = subtitle,
modifier = modifier,
cancelText = cancelText,
submitText = submitText,
onCancelClicked = onDismissRequest,

View file

@ -84,7 +84,6 @@ private fun MultipleSelectionDialogContent(
onConfirmClicked: (List<Int>) -> Unit,
dismissButtonTitle: String,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
initialSelected: ImmutableList<Int> = persistentListOf(),
subtitle: @Composable (() -> Unit)? = null,
@ -96,7 +95,6 @@ private fun MultipleSelectionDialogContent(
SimpleAlertDialogContent(
title = title,
subtitle = subtitle,
modifier = modifier,
submitText = confirmButtonTitle,
onSubmitClicked = {
onConfirmClicked(selectedOptionIndexes.toList())

View file

@ -56,13 +56,11 @@ private fun RetryDialogContent(
content: String,
onRetry: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
title: String = RetryDialogDefaults.title,
retryText: String = RetryDialogDefaults.retryText,
dismissText: String = RetryDialogDefaults.dismissText,
) {
SimpleAlertDialogContent(
modifier = modifier,
title = title,
content = content,
submitText = retryText,

View file

@ -79,7 +79,6 @@ private fun SingleSelectionDialogContent(
onOptionSelected: (Int) -> Unit,
dismissButtonTitle: String,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
initialSelection: Int? = null,
subtitle: @Composable (() -> Unit)? = null,
@ -87,7 +86,6 @@ private fun SingleSelectionDialogContent(
SimpleAlertDialogContent(
title = title,
subtitle = subtitle,
modifier = modifier,
submitText = dismissButtonTitle,
onSubmitClicked = onDismissRequest,
applyPaddingToContents = false,

View file

@ -52,9 +52,9 @@ fun PreferenceCategory(
}
@Composable
private fun PreferenceCategoryTitle(title: String, modifier: Modifier = Modifier) {
private fun PreferenceCategoryTitle(title: String) {
Text(
modifier = modifier.padding(
modifier = Modifier.padding(
top = 20.dp,
bottom = 8.dp,
start = preferencePaddingHorizontal,

View file

@ -80,10 +80,8 @@ fun PreferencePage(
private fun PreferenceTopAppBar(
title: String,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
modifier = modifier,
navigationIcon = {
BackButton(onClick = onBackPressed)
},

View file

@ -91,7 +91,6 @@ private fun TextFieldDialog(
onDismissRequest: () -> Unit,
value: String?,
placeholder: String?,
modifier: Modifier = Modifier,
validation: (String?) -> Boolean = { true },
onValidationErrorMessage: String? = null,
autoSelectOnDisplay: Boolean = true,
@ -110,7 +109,6 @@ private fun TextFieldDialog(
onSubmit = { onSubmit(textFieldContents.text) },
onDismissRequest = onDismissRequest,
enabled = canSubmit,
modifier = modifier,
) {
item {
TextFieldListItem(

View file

@ -98,11 +98,9 @@ private fun IconsPreview(
title: String,
iconsList: ImmutableList<Int>,
iconNameTransform: (String) -> String,
modifier: Modifier = Modifier,
) = ElementPreview {
val context = LocalContext.current
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.modifiers
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.input.pointer.pointerInput
fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = then(
pointerInput(Unit) {
detectTapGestures(onTap = {
focusManager.clearFocus()
})
}
)

View file

@ -55,7 +55,6 @@ internal fun SimpleAlertDialogContent(
content: String,
submitText: String,
onSubmitClicked: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
subtitle: @Composable (() -> Unit)? = null,
destructiveSubmit: Boolean = false,
@ -67,7 +66,6 @@ internal fun SimpleAlertDialogContent(
icon: @Composable (() -> Unit)? = null,
) {
SimpleAlertDialogContent(
modifier = modifier,
icon = icon,
title = title,
subtitle = subtitle,
@ -92,7 +90,6 @@ internal fun SimpleAlertDialogContent(
internal fun SimpleAlertDialogContent(
submitText: String,
onSubmitClicked: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
subtitle: @Composable (() -> Unit)? = null,
destructiveSubmit: Boolean = false,
@ -148,7 +145,6 @@ internal fun SimpleAlertDialogContent(
}
}
},
modifier = modifier,
title = title?.let { titleText ->
@Composable {
Text(
@ -192,11 +188,9 @@ internal fun AlertDialogContent(
iconContentColor: Color,
titleContentColor: Color,
textContentColor: Color,
modifier: Modifier = Modifier,
applyPaddingToContents: Boolean = true,
) {
Surface(
modifier = modifier,
shape = shape,
color = containerColor,
tonalElevation = tonalElevation,

View file

@ -1,39 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import io.element.android.libraries.designsystem.BuildConfig
import timber.log.Timber
// Note the inline function below which ensures that this function is essentially
// copied at the call site to ensure that its logging only recompositions from the
// original call site.
@Composable
fun LogCompositions(tag: String, msg: String) {
if (BuildConfig.DEBUG) {
val ref = remember { Ref() }
SideEffect { ref.value++ }
Timber.tag(tag).d("Compositions: $msg ${ref.value}")
}
}
private class Ref {
var value: Int = 0
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

View file

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Before After
Before After

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="state_event_avatar_changed_too">"(аватар таксама быў зменены)"</string>
<string name="state_event_avatar_url_changed">"%1$s змяніў аватар"</string>
<string name="state_event_avatar_url_changed_by_you">"Вы змянілі свой аватар"</string>
<string name="state_event_display_name_changed_from">"%1$s змяніў сваё адлюстраванае імя з %2$s на %3$s"</string>
<string name="state_event_display_name_changed_from_by_you">"Вы змянілі сваё адлюстраванае імя з %1$s на %2$s"</string>
<string name="state_event_display_name_removed">"%1$s выдаліў сваё адлюстраванае імя (яно было %2$s)"</string>
<string name="state_event_display_name_removed_by_you">"Вы выдалілі сваё адлюстраванае імя (яно было %1$s)"</string>
<string name="state_event_display_name_set">"%1$s усталявалі сваё адлюстраванае імя на %2$s"</string>
<string name="state_event_display_name_set_by_you">"Вы ўстанавілі адлюстраванае імя на %1$s"</string>
<string name="state_event_room_avatar_changed">"%1$s змяніў аватар пакоя"</string>
<string name="state_event_room_avatar_changed_by_you">"Вы змянілі аватар пакоя"</string>
<string name="state_event_room_avatar_removed">"%1$s выдаліў(-ла) аватар пакоя"</string>
<string name="state_event_room_avatar_removed_by_you">"Вы выдалілі аватар пакоя"</string>
<string name="state_event_room_ban">"%1$s заблакіраваў %2$s"</string>
<string name="state_event_room_ban_by_you">"Вы заблакіравалі %1$s"</string>
<string name="state_event_room_created">"%1$s стварыў пакой"</string>
<string name="state_event_room_created_by_you">"Вы стварылі пакой"</string>
<string name="state_event_room_invite">"%1$s запрасіў %2$s"</string>
<string name="state_event_room_invite_accepted">"%1$s прыняў(-ла) запрашэнне"</string>
<string name="state_event_room_invite_accepted_by_you">"Вы прынялі запрашэнне"</string>
<string name="state_event_room_invite_by_you">"Вы запрасілі %1$s"</string>
<string name="state_event_room_invite_you">"%1$s запрасіў вас"</string>
<string name="state_event_room_join">"%1$s далучыўся да пакоя"</string>
<string name="state_event_room_join_by_you">"Вы далучыліся да пакоя"</string>
<string name="state_event_room_knock">"%1$s прасіў(-ла) далучыцца"</string>
<string name="state_event_room_knock_accepted">"%1$s дазволіў(-ла) %2$s далучыцца"</string>
<string name="state_event_room_knock_accepted_by_you">"%1$s дазволіў(-ла) вам далучыцца"</string>
<string name="state_event_room_knock_by_you">"Вы прасілі далучыцца"</string>
<string name="state_event_room_knock_denied">"%1$s адхіліў(-ла) %2$s запыт на далучэнне"</string>
<string name="state_event_room_knock_denied_by_you">"Вы адхілілі %1$s запыт на далучэнне"</string>
<string name="state_event_room_knock_denied_you">"%1$s адхіліў(-ла) ваш запыт на далучэнне"</string>
<string name="state_event_room_knock_retracted">"%1$s больш не зацікаўлены(-на) у далучэнні"</string>
<string name="state_event_room_knock_retracted_by_you">"Вы адмянілі запыт на далучэнне"</string>
<string name="state_event_room_leave">"%1$s выйшаў з пакоя"</string>
<string name="state_event_room_leave_by_you">"Вы выйшлі з пакоя"</string>
<string name="state_event_room_name_changed">"%1$s змяніў назву пакоя на: %2$s"</string>
<string name="state_event_room_name_changed_by_you">"Вы змянілі назву пакоя на: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s выдаліў(-ла) назву пакоя"</string>
<string name="state_event_room_name_removed_by_you">"Вы выдалілі назву пакоя"</string>
<string name="state_event_room_reject">"%1$s адхіліў запрашэнне"</string>
<string name="state_event_room_reject_by_you">"Вы адхілілі запрашэнне"</string>
<string name="state_event_room_remove">"%1$s выдаліў %2$s"</string>
<string name="state_event_room_remove_by_you">"Вы выдалілі %1$s"</string>
<string name="state_event_room_third_party_invite">"%1$s адправіў(-ла) запрашэнне %2$s далучыцца да пакоя"</string>
<string name="state_event_room_third_party_invite_by_you">"Вы адправілі запрашэнне %1$s далучыцца да пакоя"</string>
<string name="state_event_room_third_party_revoked_invite">"%1$s адклікаў(-ла) запрашэнне для %2$s далучыцца да пакоя"</string>
<string name="state_event_room_third_party_revoked_invite_by_you">"Вы адклікалі запрашэнне для %1$s далучыцца да пакоя"</string>
<string name="state_event_room_topic_changed">"%1$s змяніў тэму на: %2$s"</string>
<string name="state_event_room_topic_changed_by_you">"Вы змянілі тэму на: %1$s"</string>
<string name="state_event_room_topic_removed">"%1$s выдаліў(-ла) тэму пакоя"</string>
<string name="state_event_room_topic_removed_by_you">"Вы выдалілі тэму пакоя"</string>
<string name="state_event_room_unban">"%1$s разблакіраваў %2$s"</string>
<string name="state_event_room_unban_by_you">"Вы разблакіравалі %1$s"</string>
<string name="state_event_room_unknown_membership_change">"%1$s унеслі невядомую змену ў сяброўства"</string>
</resources>

View file

@ -39,6 +39,8 @@
<string name="state_event_room_name_changed_by_you">"Du hast den Raumnamen geändert in: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s hat den Raumnamen entfernt"</string>
<string name="state_event_room_name_removed_by_you">"Du hast den Raumnamen entfernt"</string>
<string name="state_event_room_none">"%1$shat keine Änderungen vorgenommen"</string>
<string name="state_event_room_none_by_you">"Du hast keine Änderungen vorgenommen"</string>
<string name="state_event_room_reject">"%1$s hat die Einladung abgelehnt"</string>
<string name="state_event_room_reject_by_you">"Du hast die Einladung abgelehnt"</string>
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string>

View file

@ -39,6 +39,8 @@
<string name="state_event_room_name_changed_by_you">"Megváltoztatta a szoba nevét: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s eltávolította a szoba nevét"</string>
<string name="state_event_room_name_removed_by_you">"Eltávolította a szoba nevét"</string>
<string name="state_event_room_none">"%1$s nem változtatott semmin"</string>
<string name="state_event_room_none_by_you">"Nem változtatott semmin"</string>
<string name="state_event_room_reject">"%1$s elutasította a meghívást"</string>
<string name="state_event_room_reject_by_you">"Elutasította a meghívást"</string>
<string name="state_event_room_remove">"%1$s eltávolította: %2$s"</string>

View file

@ -75,4 +75,11 @@ enum class FeatureFlags(
defaultValue = true,
isFinished = false,
),
MarkAsUnread(
key = "feature.markAsUnread",
title = "Mark as unread",
description = "Allow user to mark a room as unread",
defaultValue = true,
isFinished = false,
),
}

View file

@ -40,6 +40,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.PinUnlock -> true
FeatureFlags.Mentions -> true
FeatureFlags.SecureStorage -> true
FeatureFlags.MarkAsUnread -> false
}
} else {
false

View file

@ -49,13 +49,11 @@ fun FeatureListView(
private fun FeaturePreferenceView(
feature: FeatureUiModel,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
PreferenceCheckbox(
title = feature.title,
supportingText = feature.description,
isChecked = feature.isEnabled,
modifier = modifier,
onCheckedChange = onCheckedChange
)
}

View file

@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import kotlinx.coroutines.flow.Flow
@ -57,6 +58,7 @@ interface MatrixRoom : Closeable {
val isDm: Boolean get() = isDirect && isOneToOne
val roomInfoFlow: Flow<MatrixRoomInfo>
val roomTypingMembersFlow: Flow<List<UserId>>
/**
* The current notable tags as a Flow.
@ -158,6 +160,17 @@ interface MatrixRoom : Closeable {
suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit>
/**
* Reverts a previously set unread flag, and eventually send a Read Receipt.
* @param receiptType The type of receipt to send. If null, no Read Receipt will be sent.
*/
suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit>
/**
* Sets a flag on the room to indicate that the user has explicitly marked it as unread.
*/
suspend fun markAsUnread(): Result<Unit>
/**
* Share a location message in the room.
*

View file

@ -28,9 +28,15 @@ enum class MessageEventType {
KEY_VERIFICATION_KEY,
KEY_VERIFICATION_MAC,
KEY_VERIFICATION_DONE,
REACTION_SENT,
REACTION,
ROOM_ENCRYPTED,
ROOM_MESSAGE,
ROOM_REDACTION,
STICKER
STICKER,
POLL_END,
POLL_RESPONSE,
POLL_START,
UNSTABLE_POLL_END,
UNSTABLE_POLL_RESPONSE,
UNSTABLE_POLL_START,
}

View file

@ -27,7 +27,19 @@ data class RoomMember(
val powerLevel: Long,
val normalizedPowerLevel: Long,
val isIgnored: Boolean,
)
) {
/**
* Disambiguated display name for the RoomMember.
* If the display name is null, the user ID is returned.
* If the display name is ambiguous, the user ID is appended in parentheses.
* Otherwise, the display name is returned.
*/
val disambiguatedDisplayName: String = when {
displayName == null -> userId.value
isNameAmbiguous -> "$displayName ($userId)"
else -> displayName
}
}
enum class RoomMembershipState {
BAN,

View file

@ -28,29 +28,7 @@ import kotlinx.coroutines.flow.onEach
* It lets load rooms on demand and filter them.
*/
interface DynamicRoomList : RoomList {
sealed interface Filter {
/**
* No filter applied.
*/
data object All : Filter
/**
* Filter only the left rooms.
*/
data object AllNonLeft : Filter
/**
* Filter all rooms.
*/
data object None : Filter
/**
* Filter rooms by normalized room name.
*/
data class NormalizedMatchRoomName(val pattern: String) : Filter
}
val currentFilter: StateFlow<Filter>
val currentFilter: StateFlow<RoomListFilter>
val loadedPages: StateFlow<Int>
val pageSize: Int
@ -68,7 +46,7 @@ interface DynamicRoomList : RoomList {
* Update the filter to apply to the list.
* @param filter the filter to apply.
*/
suspend fun updateFilter(filter: Filter)
suspend fun updateFilter(filter: RoomListFilter)
}
/**

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.roomlist
sealed interface RoomListFilter {
companion object {
fun all(vararg filters: RoomListFilter): RoomListFilter {
return All(filters.toList())
}
fun any(vararg filters: RoomListFilter): RoomListFilter {
return Any(filters.toList())
}
}
data class All(
val filters: List<RoomListFilter>
) : RoomListFilter
data class Any(
val filters: List<RoomListFilter>
) : RoomListFilter
data object NonLeft : RoomListFilter
data object Unread : RoomListFilter
sealed interface Category : RoomListFilter {
data object Group : Category
data object People : Category
}
data object None : RoomListFilter
data class NormalizedMatchRoomName(
val pattern: String
) : RoomListFilter
data class FuzzyMatchRoomName(
val pattern: String
) : RoomListFilter
}

View file

@ -43,6 +43,7 @@ data class RoomSummaryDetails(
val numUnreadMessages: Int,
val numUnreadMentions: Int,
val numUnreadNotifications: Int,
val isMarkedUnread: Boolean,
val inviter: RoomMember?,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,

View file

@ -85,11 +85,7 @@ data class ProfileChangeContent(
data class StateContent(
val stateKey: String,
val content: OtherState
) : EventContent {
fun isVisibleInTimeline(): Boolean {
return content.isVisibleInTimeline()
}
}
) : EventContent
data class FailedToParseMessageLikeContent(
val eventType: String,

View file

@ -41,30 +41,4 @@ sealed interface OtherState {
data object SpaceChild : OtherState
data object SpaceParent : OtherState
data class Custom(val eventType: String) : OtherState
fun isVisibleInTimeline() = when (this) {
// Visible
is RoomAvatar,
is RoomName,
is RoomTopic,
is RoomThirdPartyInvite,
is RoomCreate,
is RoomEncryption,
is Custom -> true
// Hidden
is RoomAliases,
is RoomCanonicalAlias,
is RoomGuestAccess,
is RoomHistoryVisibility,
is RoomJoinRules,
is RoomPinnedEvents,
is RoomPowerLevels,
is RoomServerAcl,
is RoomTombstone,
is SpaceChild,
is SpaceParent,
is PolicyRuleRoom,
is PolicyRuleServer,
is PolicyRuleUser -> false
}
}

View file

@ -25,6 +25,7 @@ data class TracingFilterConfiguration(
private val targetsToLogLevel: Map<Target, LogLevel> = mapOf(
Target.HYPER to LogLevel.WARN,
Target.MATRIX_SDK_CRYPTO to LogLevel.DEBUG,
Target.MATRIX_SDK_CRYPTO_ACCOUNT to LogLevel.TRACE,
Target.MATRIX_SDK_HTTP_CLIENT to LogLevel.DEBUG,
Target.MATRIX_SDK_SLIDING_SYNC to LogLevel.TRACE,
Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.TRACE,
@ -58,6 +59,7 @@ enum class Target(open val filter: String) {
MATRIX_SDK_FFI("matrix_sdk_ffi"),
MATRIX_SDK_UNIFFI_API("matrix_sdk_ffi::uniffi_api"),
MATRIX_SDK_CRYPTO("matrix_sdk_crypto"),
MATRIX_SDK_CRYPTO_ACCOUNT("matrix_sdk_crypto::olm::account"),
MATRIX_SDK("matrix_sdk"),
MATRIX_SDK_HTTP_CLIENT("matrix_sdk::http_client"),
MATRIX_SDK_CLIENT("matrix_sdk::client"),

View file

@ -21,6 +21,7 @@ import java.util.UUID
interface CallWidgetSettingsProvider {
fun provide(
baseUrl: String,
widgetId: String = UUID.randomUUID().toString()
widgetId: String = UUID.randomUUID().toString(),
encrypted: Boolean,
): MatrixWidgetSettings
}

View file

@ -55,10 +55,12 @@ import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.util.SessionDirectoryNameProvider
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
import io.element.android.libraries.sessionstorage.api.SessionStore
@ -76,12 +78,12 @@ import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.BackupState
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.FilterStateEventType
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.PowerLevels
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.StateEventType
import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
import org.matrix.rustcomponents.sdk.use
@ -133,6 +135,7 @@ class RustMatrixClient(
sessionCoroutineScope = sessionCoroutineScope,
dispatchers = dispatchers,
).apply { start() }
private val sessionDirectoryNameProvider = SessionDirectoryNameProvider()
private val isLoggingOut = AtomicBoolean(false)
@ -203,20 +206,20 @@ class RustMatrixClient(
private val eventFilters = TimelineEventTypeFilter.exclude(
listOf(
FilterStateEventType.ROOM_ALIASES,
FilterStateEventType.ROOM_CANONICAL_ALIAS,
FilterStateEventType.ROOM_GUEST_ACCESS,
FilterStateEventType.ROOM_HISTORY_VISIBILITY,
FilterStateEventType.ROOM_JOIN_RULES,
FilterStateEventType.ROOM_PINNED_EVENTS,
FilterStateEventType.ROOM_POWER_LEVELS,
FilterStateEventType.ROOM_SERVER_ACL,
FilterStateEventType.ROOM_TOMBSTONE,
FilterStateEventType.SPACE_CHILD,
FilterStateEventType.SPACE_PARENT,
FilterStateEventType.POLICY_RULE_ROOM,
FilterStateEventType.POLICY_RULE_SERVER,
FilterStateEventType.POLICY_RULE_USER,
StateEventType.ROOM_ALIASES,
StateEventType.ROOM_CANONICAL_ALIAS,
StateEventType.ROOM_GUEST_ACCESS,
StateEventType.ROOM_HISTORY_VISIBILITY,
StateEventType.ROOM_JOIN_RULES,
StateEventType.ROOM_PINNED_EVENTS,
StateEventType.ROOM_POWER_LEVELS,
StateEventType.ROOM_SERVER_ACL,
StateEventType.ROOM_TOMBSTONE,
StateEventType.SPACE_CHILD,
StateEventType.SPACE_PARENT,
StateEventType.POLICY_RULE_ROOM,
StateEventType.POLICY_RULE_SERVER,
StateEventType.POLICY_RULE_USER,
).map(FilterTimelineEventType::State)
)
@ -270,12 +273,7 @@ class RustMatrixClient(
private suspend fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.let { roomListItem ->
if (!roomListItem.isTimelineInitialized()) {
roomListItem.initTimeline(eventFilters)
}
roomListItem.fullRoom()
}
val fullRoom = cachedRoomListItem?.fullRoomWithTimeline(filter = eventFilters)
return if (cachedRoomListItem == null || fullRoom == null) {
Timber.d("No room cached for $roomId")
null
@ -401,13 +399,12 @@ class RustMatrixClient(
}
override suspend fun getCacheSize(): Long {
// Do not use client.userId since it can throw if client has been closed (during clear cache)
return baseDirectory.getCacheSize(userID = sessionId.value)
return baseDirectory.getCacheSize()
}
override suspend fun clearCache() {
close()
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
baseDirectory.deleteSessionDirectory(deleteCryptoDb = false)
}
override suspend fun logout(ignoreSdkError: Boolean): String? = doLogout(
@ -436,7 +433,7 @@ class RustMatrixClient(
}
}
close()
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
baseDirectory.deleteSessionDirectory(deleteCryptoDb = true)
if (removeSession) {
sessionStore.removeSession(sessionId.value)
}
@ -482,12 +479,10 @@ class RustMatrixClient(
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
private suspend fun File.getCacheSize(
userID: String,
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {
// Rust sanitises the user ID replacing invalid characters with an _
val sanitisedUserID = userID.replace(":", "_")
val sessionDirectory = File(this@getCacheSize, sanitisedUserID)
val sessionDirectoryName = sessionDirectoryNameProvider.provides(sessionId)
val sessionDirectory = File(this@getCacheSize, sessionDirectoryName)
if (includeCryptoDb) {
sessionDirectory.getSizeOfFiles()
} else {
@ -504,12 +499,10 @@ class RustMatrixClient(
}
private suspend fun File.deleteSessionDirectory(
userID: String,
deleteCryptoDb: Boolean = false,
): Boolean = withContext(sessionDispatcher) {
// Rust sanitises the user ID replacing invalid characters with an _
val sanitisedUserID = userID.replace(":", "_")
val sessionDirectory = File(this@deleteSessionDirectory, sanitisedUserID)
val sessionDirectoryName = sessionDirectoryNameProvider.provides(sessionId)
val sessionDirectory = File(this@deleteSessionDirectory, sessionDirectoryName)
if (deleteCryptoDb) {
// Delete the folder and all its content
sessionDirectory.deleteRecursively()

View file

@ -31,11 +31,17 @@ fun MessageEventType.map(): MessageLikeEventType = when (this) {
MessageEventType.KEY_VERIFICATION_KEY -> MessageLikeEventType.KEY_VERIFICATION_KEY
MessageEventType.KEY_VERIFICATION_MAC -> MessageLikeEventType.KEY_VERIFICATION_MAC
MessageEventType.KEY_VERIFICATION_DONE -> MessageLikeEventType.KEY_VERIFICATION_DONE
MessageEventType.REACTION_SENT -> MessageLikeEventType.REACTION_SENT
MessageEventType.REACTION -> MessageLikeEventType.REACTION
MessageEventType.ROOM_ENCRYPTED -> MessageLikeEventType.ROOM_ENCRYPTED
MessageEventType.ROOM_MESSAGE -> MessageLikeEventType.ROOM_MESSAGE
MessageEventType.ROOM_REDACTION -> MessageLikeEventType.ROOM_REDACTION
MessageEventType.STICKER -> MessageLikeEventType.STICKER
MessageEventType.POLL_END -> MessageLikeEventType.POLL_END
MessageEventType.POLL_RESPONSE -> MessageLikeEventType.POLL_RESPONSE
MessageEventType.POLL_START -> MessageLikeEventType.POLL_START
MessageEventType.UNSTABLE_POLL_END -> MessageLikeEventType.UNSTABLE_POLL_END
MessageEventType.UNSTABLE_POLL_RESPONSE -> MessageLikeEventType.UNSTABLE_POLL_RESPONSE
MessageEventType.UNSTABLE_POLL_START -> MessageLikeEventType.UNSTABLE_POLL_START
}
fun MessageLikeEventType.map(): MessageEventType = when (this) {
@ -50,9 +56,15 @@ fun MessageLikeEventType.map(): MessageEventType = when (this) {
MessageLikeEventType.KEY_VERIFICATION_KEY -> MessageEventType.KEY_VERIFICATION_KEY
MessageLikeEventType.KEY_VERIFICATION_MAC -> MessageEventType.KEY_VERIFICATION_MAC
MessageLikeEventType.KEY_VERIFICATION_DONE -> MessageEventType.KEY_VERIFICATION_DONE
MessageLikeEventType.REACTION_SENT -> MessageEventType.REACTION_SENT
MessageLikeEventType.REACTION -> MessageEventType.REACTION
MessageLikeEventType.ROOM_ENCRYPTED -> MessageEventType.ROOM_ENCRYPTED
MessageLikeEventType.ROOM_MESSAGE -> MessageEventType.ROOM_MESSAGE
MessageLikeEventType.ROOM_REDACTION -> MessageEventType.ROOM_REDACTION
MessageLikeEventType.STICKER -> MessageEventType.STICKER
MessageLikeEventType.POLL_END -> MessageEventType.POLL_END
MessageLikeEventType.POLL_RESPONSE -> MessageEventType.POLL_RESPONSE
MessageLikeEventType.POLL_START -> MessageEventType.POLL_START
MessageLikeEventType.UNSTABLE_POLL_END -> MessageEventType.UNSTABLE_POLL_END
MessageLikeEventType.UNSTABLE_POLL_RESPONSE -> MessageEventType.UNSTABLE_POLL_RESPONSE
MessageLikeEventType.UNSTABLE_POLL_START -> MessageEventType.UNSTABLE_POLL_START
}

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.ForwardEventException
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered
import kotlinx.coroutines.CancellationException
@ -50,7 +51,9 @@ class RoomContentForwarder(
) {
val content = fromTimeline.getTimelineEventContentByEventId(eventId.value)
val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
val targetRooms = targetSlidingSyncRooms.mapNotNull { slidingSyncRoom -> slidingSyncRoom.use { it.fullRoom() } }
val targetRooms = targetSlidingSyncRooms.map { slidingSyncRoom ->
slidingSyncRoom.use { it.fullRoomWithTimeline(null) }
}
val failedForwardingTo = mutableSetOf<RoomId>()
targetRooms.parallelMap { room ->
room.use { targetRoom ->

View file

@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
@ -53,6 +54,7 @@ import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
import io.element.android.libraries.matrix.impl.room.tags.map
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
@ -74,6 +76,7 @@ import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
import org.matrix.rustcomponents.sdk.RoomNotableTagsListener
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.TypingNotificationsListener
import org.matrix.rustcomponents.sdk.WidgetCapabilities
import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
@ -115,6 +118,7 @@ class RustMatrixRoom(
})
}
override val notableTagsFlow: Flow<RoomNotableTags> = mxCallbackFlow {
innerRoom.subscribeToNotableTags(object : RoomNotableTagsListener {
override fun call(notableTags: RustRoomNotableTags) {
@ -123,6 +127,22 @@ class RustMatrixRoom(
})
}
override val roomTypingMembersFlow: Flow<List<UserId>> = mxCallbackFlow {
launch {
val initial = emptyList<UserId>()
channel.trySend(initial)
}
innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener {
override fun call(typingUserIds: List<String>) {
channel.trySend(
typingUserIds
.filter { it != sessionData.userId }
.map(::UserId)
)
}
})
}
// Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)
@ -441,6 +461,22 @@ class RustMatrixRoom(
}
}
override suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
if (receiptType != null) {
innerRoom.markAsReadAndSendReadReceipt(receiptType.toRustReceiptType())
} else {
innerRoom.markAsRead()
}
}
}
override suspend fun markAsUnread(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.markAsUnread()
}
}
override suspend fun sendLocation(
body: String,
geoUri: String,

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@ -28,7 +29,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
@ -44,7 +44,7 @@ internal class RoomListFactory(
*/
fun createRoomList(
pageSize: Int,
initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.All,
initialFilter: RoomListFilter = RoomListFilter.all(),
innerProvider: suspend () -> InnerRoomList
): DynamicRoomList {
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
@ -91,7 +91,7 @@ internal class RoomListFactory(
private class RustDynamicRoomList(
override val summaries: MutableStateFlow<List<RoomSummary>>,
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
override val currentFilter: MutableStateFlow<DynamicRoomList.Filter>,
override val currentFilter: MutableStateFlow<RoomListFilter>,
override val loadedPages: MutableStateFlow<Int>,
private val dynamicEvents: MutableSharedFlow<RoomListDynamicEvents>,
private val processor: RoomSummaryListProcessor,
@ -101,7 +101,7 @@ private class RustDynamicRoomList(
processor.rebuildRoomSummaries()
}
override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
override suspend fun updateFilter(filter: RoomListFilter) {
currentFilter.emit(filter)
val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter())
dynamicEvents.emit(filterEvent)
@ -124,12 +124,3 @@ private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
RoomListLoadingState.NotLoaded -> RoomList.LoadingState.NotLoaded
}
}
private fun DynamicRoomList.Filter.toRustFilter(): RoomListEntriesDynamicFilterKind {
return when (this) {
DynamicRoomList.Filter.All -> RoomListEntriesDynamicFilterKind.All
is DynamicRoomList.Filter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(this.pattern)
DynamicRoomList.Filter.None -> RoomListEntriesDynamicFilterKind.None
DynamicRoomList.Filter.AllNonLeft -> RoomListEntriesDynamicFilterKind.AllNonLeft
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListFilterCategory
fun RoomListFilter.toRustFilter(): RoomListEntriesDynamicFilterKind {
return when (this) {
is RoomListFilter.All -> RoomListEntriesDynamicFilterKind.All(filters.map { it.toRustFilter() })
is RoomListFilter.Any -> RoomListEntriesDynamicFilterKind.Any(filters.map { it.toRustFilter() })
RoomListFilter.Category.Group -> RoomListEntriesDynamicFilterKind.Category(RoomListFilterCategory.GROUP)
RoomListFilter.Category.People -> RoomListEntriesDynamicFilterKind.Category(RoomListFilterCategory.PEOPLE)
is RoomListFilter.FuzzyMatchRoomName -> RoomListEntriesDynamicFilterKind.FuzzyMatchRoomName(pattern)
RoomListFilter.NonLeft -> RoomListEntriesDynamicFilterKind.NonLeft
RoomListFilter.None -> RoomListEntriesDynamicFilterKind.None
is RoomListFilter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(pattern)
RoomListFilter.Unread -> RoomListEntriesDynamicFilterKind.Unread
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.roomlist
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
/** Returns a `Room` with an initialized timeline using the given [filter]. */
suspend fun RoomListItem.fullRoomWithTimeline(filter: TimelineEventTypeFilter? = null): Room {
if (!isTimelineInitialized()) {
initTimeline(filter)
}
return fullRoom()
}

View file

@ -38,6 +38,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
numUnreadMentions = roomInfo.numUnreadMentions.toInt(),
numUnreadMessages = roomInfo.numUnreadMessages.toInt(),
numUnreadNotifications = roomInfo.numUnreadNotifications.toInt(),
isMarkedUnread = roomInfo.isMarkedUnread,
lastMessage = latestRoomMessage,
inviter = roomInfo.inviter?.let(RoomMemberMapper::map),
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode),

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
import kotlinx.coroutines.CoroutineScope
@ -45,7 +46,7 @@ internal class RustRoomListService(
) : RoomListService {
override val allRooms: DynamicRoomList = roomListFactory.createRoomList(
pageSize = DEFAULT_PAGE_SIZE,
initialFilter = DynamicRoomList.Filter.AllNonLeft,
initialFilter = RoomListFilter.all(RoomListFilter.NonLeft),
) {
innerRoomListService.allRooms()
}

View file

@ -28,7 +28,6 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelin
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.postprocessor.DmBeginningTimelineProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterHiddenStateEventsProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
@ -84,8 +83,6 @@ class RustMatrixTimeline(
dispatcher = dispatcher,
)
private val filterHiddenStateEventsProcessor = FilterHiddenStateEventsProcessor()
private val dmBeginningTimelineProcessor = DmBeginningTimelineProcessor()
private val timelineItemFactory = MatrixTimelineItemMapper(
@ -109,7 +106,6 @@ class RustMatrixTimeline(
@OptIn(ExperimentalCoroutinesApi::class)
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
.mapLatest { items -> encryptedHistoryPostProcessor.process(items) }
.mapLatest { items -> filterHiddenStateEventsProcessor.process(items) }
.mapLatest { items ->
dmBeginningTimelineProcessor.process(
items = items,

View file

@ -1,42 +0,0 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
/**
* This class is used to filter out 'hidden' state events from the timeline.
*/
class FilterHiddenStateEventsProcessor {
fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
return items.filter { item ->
when (item) {
is MatrixTimelineItem.Event -> {
when (val content = item.event.content) {
// If it's a state event, make sure it's visible
is StateContent -> content.isVisibleInTimeline()
// We can display any other event
else -> true
}
}
is MatrixTimelineItem.Virtual -> true
is MatrixTimelineItem.Other -> true
}
}
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.util
import io.element.android.libraries.matrix.api.core.SessionId
class SessionDirectoryNameProvider {
// Rust sanitises the user ID replacing invalid characters with an _
fun provides(sessionId: SessionId): String {
return sessionId.value.replace(":", "_")
}
}

View file

@ -27,7 +27,7 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettingsProvider {
override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings {
override fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
val options = VirtualElementCallWidgetOptions(
elementCallUrl = baseUrl,
widgetId = widgetId,
@ -40,7 +40,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettin
confineToRoom = true,
font = null,
analyticsId = null,
encryption = EncryptionSystem.PerParticipantKeys,
encryption = if (encrypted) EncryptionSystem.PerParticipantKeys else EncryptionSystem.Unencrypted,
)
val rustWidgetSettings = newVirtualElementCallWidget(options)
return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings)

View file

@ -1,77 +0,0 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import org.junit.Test
class FilterHiddenStateEventsProcessorTest {
@Test
fun test() {
val items = listOf(
// These are visible because they're not state events
MatrixTimelineItem.Other,
MatrixTimelineItem.Virtual("virtual", VirtualTimelineItem.ReadMarker),
MatrixTimelineItem.Event("event", anEventTimelineItem()),
// These are visible state events
MatrixTimelineItem.Event("m.room.avatar", anEventTimelineItem(content = StateContent("", OtherState.RoomAvatar("")))),
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.encrypted", anEventTimelineItem(content = StateContent("", OtherState.RoomEncryption))),
MatrixTimelineItem.Event("m.room.name", anEventTimelineItem(content = StateContent("", OtherState.RoomName("")))),
MatrixTimelineItem.Event("m.room.third_party_invite", anEventTimelineItem(content = StateContent("", OtherState.RoomThirdPartyInvite("")))),
MatrixTimelineItem.Event("m.room.topic", anEventTimelineItem(content = StateContent("", OtherState.RoomTopic("")))),
MatrixTimelineItem.Event("m.room.custom", anEventTimelineItem(content = StateContent("", OtherState.Custom("")))),
// These ones are hidden
MatrixTimelineItem.Event("m.room.aliases", anEventTimelineItem(content = StateContent("", OtherState.RoomAliases))),
MatrixTimelineItem.Event("m.room.canonical_alias", anEventTimelineItem(content = StateContent("", OtherState.RoomCanonicalAlias))),
MatrixTimelineItem.Event("m.room.guest_access", anEventTimelineItem(content = StateContent("", OtherState.RoomGuestAccess))),
MatrixTimelineItem.Event("m.room.history_visibility", anEventTimelineItem(content = StateContent("", OtherState.RoomHistoryVisibility))),
MatrixTimelineItem.Event("m.room.join_rules", anEventTimelineItem(content = StateContent("", OtherState.RoomJoinRules))),
MatrixTimelineItem.Event("m.room.pinned_events", anEventTimelineItem(content = StateContent("", OtherState.RoomPinnedEvents))),
MatrixTimelineItem.Event("m.room.power_levels", anEventTimelineItem(content = StateContent("", OtherState.RoomPowerLevels))),
MatrixTimelineItem.Event("m.room.server_acl", anEventTimelineItem(content = StateContent("", OtherState.RoomServerAcl))),
MatrixTimelineItem.Event("m.room.tombstone", anEventTimelineItem(content = StateContent("", OtherState.RoomTombstone))),
MatrixTimelineItem.Event("m.space.child", anEventTimelineItem(content = StateContent("", OtherState.SpaceChild))),
MatrixTimelineItem.Event("m.space.parent", anEventTimelineItem(content = StateContent("", OtherState.SpaceParent))),
MatrixTimelineItem.Event("m.room.policy.rule.room", anEventTimelineItem(content = StateContent("", OtherState.PolicyRuleRoom))),
MatrixTimelineItem.Event("m.room.policy.rule.server", anEventTimelineItem(content = StateContent("", OtherState.PolicyRuleServer))),
MatrixTimelineItem.Event("m.room.policy.rule.user", anEventTimelineItem(content = StateContent("", OtherState.PolicyRuleUser))),
)
val expected = listOf(
MatrixTimelineItem.Other,
MatrixTimelineItem.Virtual("virtual", VirtualTimelineItem.ReadMarker),
MatrixTimelineItem.Event("event", anEventTimelineItem()),
MatrixTimelineItem.Event("m.room.avatar", anEventTimelineItem(content = StateContent("", OtherState.RoomAvatar("")))),
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.encrypted", anEventTimelineItem(content = StateContent("", OtherState.RoomEncryption))),
MatrixTimelineItem.Event("m.room.name", anEventTimelineItem(content = StateContent("", OtherState.RoomName("")))),
MatrixTimelineItem.Event("m.room.third_party_invite", anEventTimelineItem(content = StateContent("", OtherState.RoomThirdPartyInvite("")))),
MatrixTimelineItem.Event("m.room.topic", anEventTimelineItem(content = StateContent("", OtherState.RoomTopic("")))),
MatrixTimelineItem.Event("m.room.custom", anEventTimelineItem(content = StateContent("", OtherState.Custom("")))),
)
val processor = FilterHiddenStateEventsProcessor()
assertThat(processor.process(items)).isEqualTo(expected)
}
}

View file

@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
@ -174,6 +175,9 @@ class FakeMatrixRoom(
private val _notableTagsFlow: MutableStateFlow<RoomNotableTags> = MutableStateFlow(aRoomNotableTags())
override val notableTagsFlow: Flow<RoomNotableTags> = _notableTagsFlow
private val _roomTypingMembersFlow: MutableSharedFlow<List<UserId>> = MutableSharedFlow(replay = 1)
override val roomTypingMembersFlow: Flow<List<UserId>> = _roomTypingMembersFlow
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> =
@ -393,6 +397,22 @@ class FakeMatrixRoom(
}
}
val markAsReadCalls = mutableListOf<ReceiptType?>()
override suspend fun markAsRead(receiptType: ReceiptType?): Result<Unit> {
markAsReadCalls.add(receiptType)
return Result.success(Unit)
}
var markAsUnreadReadCallCount = 0
private set
override suspend fun markAsUnread(): Result<Unit> {
markAsUnreadReadCallCount++
return Result.success(Unit)
}
override suspend fun sendLocation(
body: String,
geoUri: String,
@ -597,6 +617,10 @@ class FakeMatrixRoom(
fun givenRoomInfo(roomInfo: MatrixRoomInfo) {
_roomInfoFlow.tryEmit(roomInfo)
}
fun givenRoomTypingMembers(typingMembers: List<UserId>) {
_roomTypingMembersFlow.tryEmit(typingMembers)
}
}
data class SendLocationInvocation(

View file

@ -37,8 +37,8 @@ fun aRoomSummaryFilled(
isDirect: Boolean = false,
avatarUrl: String? = null,
lastMessage: RoomMessage? = aRoomMessage(),
numUnreadMentions: Int = 1,
numUnreadMessages: Int = 2,
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
notificationMode: RoomNotificationMode? = null,
) = RoomSummary.Filled(
aRoomSummaryDetails(
@ -62,6 +62,7 @@ fun aRoomSummaryDetails(
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
numUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
notificationMode: RoomNotificationMode? = null,
inviter: RoomMember? = null,
canonicalAlias: String? = null,
@ -76,6 +77,7 @@ fun aRoomSummaryDetails(
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
userDefinedNotificationMode = notificationMode,
inviter = inviter,
canonicalAlias = canonicalAlias,

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.test.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.MutableStateFlow
@ -61,13 +62,13 @@ class FakeRoomListService : RoomListService {
override val allRooms: DynamicRoomList = SimplePagedRoomList(
allRoomSummariesFlow,
allRoomsLoadingStateFlow,
MutableStateFlow(DynamicRoomList.Filter.None)
MutableStateFlow(RoomListFilter.all())
)
override val invites: RoomList = SimplePagedRoomList(
inviteRoomSummariesFlow,
inviteRoomsLoadingStateFlow,
MutableStateFlow(DynamicRoomList.Filter.None)
MutableStateFlow(RoomListFilter.all())
)
override fun updateAllRoomsVisibleRange(range: IntRange) {

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.test.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -26,7 +27,7 @@ import kotlinx.coroutines.flow.getAndUpdate
data class SimplePagedRoomList(
override val summaries: StateFlow<List<RoomSummary>>,
override val loadingState: StateFlow<RoomList.LoadingState>,
override val currentFilter: MutableStateFlow<DynamicRoomList.Filter>
override val currentFilter: MutableStateFlow<RoomListFilter>
) : DynamicRoomList {
override val pageSize: Int = Int.MAX_VALUE
override val loadedPages = MutableStateFlow(1)
@ -40,7 +41,7 @@ data class SimplePagedRoomList(
loadedPages.emit(1)
}
override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
override suspend fun updateFilter(filter: RoomListFilter) {
currentFilter.emit(filter)
}

View file

@ -24,7 +24,7 @@ class FakeCallWidgetSettingsProvider(
) : CallWidgetSettingsProvider {
val providedBaseUrls = mutableListOf<String>()
override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings {
override fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
providedBaseUrls += baseUrl
return provideFn(baseUrl, widgetId)
}

View file

@ -116,6 +116,7 @@ fun aRoomSummaryDetails(
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
numUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
) = RoomSummaryDetails(
roomId = roomId,
name = name,
@ -130,4 +131,5 @@ fun aRoomSummaryDetails(
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
)

View file

@ -86,7 +86,6 @@ private fun PdfPagesView(
@Composable
private fun PdfPageView(
pdfPage: PdfPage,
modifier: Modifier = Modifier,
) {
val pdfPageState by pdfPage.stateFlow.collectAsState()
DisposableEffect(pdfPage) {
@ -101,12 +100,12 @@ private fun PdfPageView(
bitmap = state.bitmap.asImageBitmap(),
contentDescription = stringResource(id = CommonStrings.a11y_page_n, pdfPage.pageIndex),
contentScale = ContentScale.FillWidth,
modifier = modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
)
}
is PdfPage.State.Loading -> {
Box(
modifier = modifier
modifier = Modifier
.fillMaxWidth()
.height(state.height.toDp())
.background(color = Color.White)

View file

@ -259,10 +259,8 @@ private fun ErrorView(
errorMessage: String,
onRetry: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
RetryDialog(
modifier = modifier,
content = errorMessage,
onRetry = onRetry,
onDismiss = onDismiss

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="dialog_permission_camera">"Каб дазволіць праграме выкарыстоўваць камеру, дайце дазвол у наладах сістэмы."</string>
<string name="dialog_permission_generic">"Калі ласка, дайце дазвол у наладах сістэмы."</string>
<string name="dialog_permission_microphone">"Каб дазволіць праграме выкарыстоўваць мікрафон, дайце дазвол у наладах сістэмы."</string>
<string name="dialog_permission_notification">"Каб дазволіць праграме паказваць апавяшчэнні, дайце дазвол у наладах сістэмы."</string>
</resources>

View file

@ -19,8 +19,20 @@ package io.element.android.features.preferences.api.store
import kotlinx.coroutines.flow.Flow
interface SessionPreferencesStore {
suspend fun setSharePresence(enabled: Boolean)
fun isSharePresenceEnabled(): Flow<Boolean>
suspend fun setSendPublicReadReceipts(enabled: Boolean)
fun isSendPublicReadReceiptsEnabled(): Flow<Boolean>
suspend fun setRenderReadReceipts(enabled: Boolean)
fun isRenderReadReceiptsEnabled(): Flow<Boolean>
suspend fun setSendTypingNotifications(enabled: Boolean)
fun isSendTypingNotificationsEnabled(): Flow<Boolean>
suspend fun setRenderTypingNotifications(enabled: Boolean)
fun isRenderTypingNotificationsEnabled(): Flow<Boolean>
suspend fun clear()
}

View file

@ -29,7 +29,9 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import java.io.File
class DefaultSessionPreferencesStore(
@ -43,13 +45,41 @@ class DefaultSessionPreferencesStore(
return context.preferencesDataStoreFile("session_${hashedUserId}_preferences")
}
}
private val sharePresenceKey = booleanPreferencesKey("sharePresence")
private val sendPublicReadReceiptsKey = booleanPreferencesKey("sendPublicReadReceipts")
private val renderReadReceiptsKey = booleanPreferencesKey("renderReadReceipts")
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
private val dataStoreFile = storeFile(context, sessionId)
private val store = PreferenceDataStoreFactory.create(scope = sessionCoroutineScope) { dataStoreFile }
override suspend fun setSharePresence(enabled: Boolean) {
update(sharePresenceKey, enabled)
// Also update all the other settings
setSendPublicReadReceipts(enabled)
setRenderReadReceipts(enabled)
setSendTypingNotifications(enabled)
setRenderTypingNotifications(enabled)
}
override fun isSharePresenceEnabled(): Flow<Boolean> {
// Migration, if sendPublicReadReceiptsKey was false, consider that sharing presence is false.
return get(sharePresenceKey) { runBlocking { isSendPublicReadReceiptsEnabled().first() } }
}
override suspend fun setSendPublicReadReceipts(enabled: Boolean) = update(sendPublicReadReceiptsKey, enabled)
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = get(sendPublicReadReceiptsKey, true)
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = get(sendPublicReadReceiptsKey) { true }
override suspend fun setRenderReadReceipts(enabled: Boolean) = update(renderReadReceiptsKey, enabled)
override fun isRenderReadReceiptsEnabled(): Flow<Boolean> = get(renderReadReceiptsKey) { true }
override suspend fun setSendTypingNotifications(enabled: Boolean) = update(sendTypingNotificationsKey, enabled)
override fun isSendTypingNotificationsEnabled(): Flow<Boolean> = get(sendTypingNotificationsKey) { true }
override suspend fun setRenderTypingNotifications(enabled: Boolean) = update(renderTypingNotificationsKey, enabled)
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = get(renderTypingNotificationsKey) { true }
override suspend fun clear() {
dataStoreFile.safeDelete()
@ -59,7 +89,7 @@ class DefaultSessionPreferencesStore(
store.edit { prefs -> prefs[key] = value }
}
private fun <T> get(key: Preferences.Key<T>, default: T): Flow<T> {
return store.data.map { prefs -> prefs[key] ?: default }
private fun <T> get(key: Preferences.Key<T>, default: () -> T): Flow<T> {
return store.data.map { prefs -> prefs[key] ?: default() }
}
}

View file

@ -21,6 +21,8 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import kotlinx.coroutines.CoroutineScope
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@ -28,9 +30,20 @@ import javax.inject.Inject
@SingleIn(AppScope::class)
class DefaultSessionPreferencesStoreFactory @Inject constructor(
@ApplicationContext private val context: Context,
sessionObserver: SessionObserver,
) {
private val cache = ConcurrentHashMap<SessionId, DefaultSessionPreferencesStore>()
init {
sessionObserver.addListener(object : SessionListener {
override suspend fun onSessionCreated(userId: String) = Unit
override suspend fun onSessionDeleted(userId: String) {
val sessionPreferences = cache.remove(SessionId(userId))
sessionPreferences?.clear()
}
})
}
fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): DefaultSessionPreferencesStore = cache.getOrPut(sessionId) {
DefaultSessionPreferencesStore(context, sessionId, sessionCoroutineScope)
}

View file

@ -21,19 +21,50 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class InMemorySessionPreferencesStore(
isSharePresenceEnabled: Boolean = true,
isSendPublicReadReceiptsEnabled: Boolean = true,
isRenderReadReceiptsEnabled: Boolean = true,
isSendTypingNotificationsEnabled: Boolean = true,
isRenderTypingNotificationsEnabled: Boolean = true,
) : SessionPreferencesStore {
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
private val isRenderReadReceiptsEnabled = MutableStateFlow(isRenderReadReceiptsEnabled)
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
var clearCallCount = 0
private set
override suspend fun setSharePresence(enabled: Boolean) {
isSharePresenceEnabled.tryEmit(enabled)
}
override fun isSharePresenceEnabled(): Flow<Boolean> = isSharePresenceEnabled
override suspend fun setSendPublicReadReceipts(enabled: Boolean) {
isSendPublicReadReceiptsEnabled.tryEmit(enabled)
}
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> {
return isSendPublicReadReceiptsEnabled
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = isSendPublicReadReceiptsEnabled
override suspend fun setRenderReadReceipts(enabled: Boolean) {
isRenderReadReceiptsEnabled.tryEmit(enabled)
}
override fun isRenderReadReceiptsEnabled(): Flow<Boolean> = isRenderReadReceiptsEnabled
override suspend fun setSendTypingNotifications(enabled: Boolean) {
isSendTypingNotificationsEnabled.tryEmit(enabled)
}
override fun isSendTypingNotificationsEnabled(): Flow<Boolean> = isSendTypingNotificationsEnabled
override suspend fun setRenderTypingNotifications(enabled: Boolean) {
isRenderTypingNotificationsEnabled.tryEmit(enabled)
}
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = isRenderTypingNotificationsEnabled
override suspend fun clear() {
clearCallCount++
isSendPublicReadReceiptsEnabled.tryEmit(true)

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_call">"Выклік"</string>
<string name="notification_channel_listening_for_events">"Праслухоўванне падзей"</string>
<string name="notification_channel_noisy">"Шумныя апавяшчэнні"</string>
<string name="notification_channel_silent">"Ціхія апавяшчэнні"</string>
<string name="notification_inline_reply_failed">"** Не атрымалася даслаць - калі ласка, адкрыйце пакой"</string>
<string name="notification_invitation_action_reject">"Адхіліць"</string>
<string name="notification_invite_body">"Запрасіў вас у чат"</string>
<string name="notification_mentioned_you_body">"Згадаў вас: %1$s"</string>
<string name="notification_new_messages">"Новыя паведамленні"</string>
<string name="notification_reaction_body">"Адрэагаваў на %1$s"</string>
<string name="notification_room_invite_body">"Запрасіў вас далучыцца да пакоя"</string>
<string name="notification_sender_me">"Я"</string>
<string name="notification_test_push_notification_content">"Вы праглядаеце апавяшчэнне! Націсніце мяне!"</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>
<string name="notification_unread_notified_messages_and_invitation">"%1$s і %2$s"</string>
<string name="notification_unread_notified_messages_in_room">"%1$s у %2$s"</string>
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s у %2$s і %3$s"</string>
<plurals name="notification_compat_summary_line_for_room">
<item quantity="one">"%1$s: %2$d паведамленне"</item>
<item quantity="few">"%1$s: %2$d паведамленняў"</item>
<item quantity="many">"%1$s: %2$d паведамленняў"</item>
</plurals>
<plurals name="notification_compat_summary_title">
<item quantity="one">"%d апавяшчэнне"</item>
<item quantity="few">"%d апавяшчэнняў"</item>
<item quantity="many">"%d апавяшчэнняў"</item>
</plurals>
<plurals name="notification_invitations">
<item quantity="one">"%d запрашэнне"</item>
<item quantity="few">"%d запрашэнняў"</item>
<item quantity="many">"%d запрашэнняў"</item>
</plurals>
<plurals name="notification_new_messages_for_room">
<item quantity="one">"%d новае паведамленне"</item>
<item quantity="few">"%d новых паведамленняў"</item>
<item quantity="many">"%d новых паведамленняў"</item>
</plurals>
<plurals name="notification_unread_notified_messages">
<item quantity="one">"%d непрачытанае апавяшчэнне"</item>
<item quantity="few">"%d непрачытаных апавяшчэнняў"</item>
<item quantity="many">"%d непрачытаных апавяшчэнняў"</item>
</plurals>
<plurals name="notification_unread_notified_messages_in_room_rooms">
<item quantity="one">"%d пакой"</item>
<item quantity="few">"%d пакояў"</item>
<item quantity="many">"%d пакояў"</item>
</plurals>
<string name="push_choose_distributor_dialog_title_android">"Выберыце спосаб атрымання апавяшчэнняў"</string>
<string name="push_distributor_background_sync_android">"Фонавая сінхранізацыя"</string>
<string name="push_distributor_firebase_android">"Сэрвісы Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Службы Google Play не знойдзены. Апавяшчэнні могуць не працаваць належным чынам."</string>
<string name="notification_fallback_content">"Апавяшчэнне"</string>
<string name="notification_invitation_action_join">"Далучыцца"</string>
<string name="notification_room_action_quick_reply">"Хуткі адказ"</string>
</resources>

View file

@ -5,13 +5,11 @@
<string name="notification_channel_noisy">"Hlasitá oznámení"</string>
<string name="notification_channel_silent">"Tichá oznámení"</string>
<string name="notification_inline_reply_failed">"** Nepodařilo se odeslat - otevřete prosím místnost"</string>
<string name="notification_invitation_action_join">"Vstoupit"</string>
<string name="notification_invitation_action_reject">"Odmítnout"</string>
<string name="notification_invite_body">"Vás pozval(a) do chatu"</string>
<string name="notification_mentioned_you_body">"Zmínili vás: %1$s"</string>
<string name="notification_new_messages">"Nové zprávy"</string>
<string name="notification_reaction_body">"Reagoval(a) s %1$s"</string>
<string name="notification_room_action_mark_as_read">"Označit jako přečtené"</string>
<string name="notification_room_invite_body">"Vás pozval(a) do místnosti"</string>
<string name="notification_sender_me">"Já"</string>
<string name="notification_test_push_notification_content">"Prohlížíte si oznámení! Klikněte na mě!"</string>
@ -55,5 +53,7 @@
<string name="push_distributor_firebase_android">"Služby Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Nebyly nalezeny žádné funkční služby Google Play. Oznámení nemusí fungovat správně."</string>
<string name="notification_fallback_content">"Oznámení"</string>
<string name="notification_invitation_action_join">"Vstoupit"</string>
<string name="notification_room_action_mark_as_read">"Označit jako přečtené"</string>
<string name="notification_room_action_quick_reply">"Rychlá odpověď"</string>
</resources>

View file

@ -5,13 +5,11 @@
<string name="notification_channel_noisy">"Laute Benachrichtigungen"</string>
<string name="notification_channel_silent">"Stumme Benachrichtigungen"</string>
<string name="notification_inline_reply_failed">"** Fehler beim Senden - bitte Raum öffnen"</string>
<string name="notification_invitation_action_join">"Beitreten"</string>
<string name="notification_invitation_action_reject">"Ablehnen"</string>
<string name="notification_invite_body">"Du wurdest zu einem Chat eingeladen"</string>
<string name="notification_mentioned_you_body">"Hat Dich erwähnt: %1$s"</string>
<string name="notification_new_messages">"Neue Nachrichten"</string>
<string name="notification_reaction_body">"Reagiert mit %1$s"</string>
<string name="notification_room_action_mark_as_read">"Als gelesen markieren"</string>
<string name="notification_room_invite_body">"Du wurdest eingeladen, den Raum zu betreten"</string>
<string name="notification_sender_me">"Ich"</string>
<string name="notification_test_push_notification_content">"Du siehst dir die Benachrichtigung an! Klicke hier!"</string>
@ -49,5 +47,7 @@
<string name="push_distributor_firebase_android">"Google-Dienste"</string>
<string name="push_no_valid_google_play_services_apk_android">"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."</string>
<string name="notification_fallback_content">"Mitteilung"</string>
<string name="notification_invitation_action_join">"Beitreten"</string>
<string name="notification_room_action_mark_as_read">"Als gelesen markieren"</string>
<string name="notification_room_action_quick_reply">"Schnelle Antwort"</string>
</resources>

View file

@ -5,13 +5,11 @@
<string name="notification_channel_noisy">"Notificaciones ruidosas"</string>
<string name="notification_channel_silent">"Notificaciones silenciosas"</string>
<string name="notification_inline_reply_failed">"** No se ha podido enviar - por favor, abre la sala"</string>
<string name="notification_invitation_action_join">"Unirse"</string>
<string name="notification_invitation_action_reject">"Rechazar"</string>
<string name="notification_invite_body">"Te invitó a chatear"</string>
<string name="notification_mentioned_you_body">"Te mencionó: %1$s"</string>
<string name="notification_new_messages">"Mensajes nuevos"</string>
<string name="notification_reaction_body">"Reaccionó con %1$s"</string>
<string name="notification_room_action_mark_as_read">"Marcar como leído"</string>
<string name="notification_room_invite_body">"Te invitó a unirte a la sala"</string>
<string name="notification_sender_me">"Yo"</string>
<string name="notification_test_push_notification_content">"¡Estás viendo la notificación! ¡Haz clic en mí!"</string>
@ -49,5 +47,6 @@
<string name="push_distributor_firebase_android">"Servicios de Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"No se han encontrado Servicios de Google Play válidos. Es posible que las notificaciones no funcionen correctamente."</string>
<string name="notification_fallback_content">"Notificación"</string>
<string name="notification_invitation_action_join">"Unirse"</string>
<string name="notification_room_action_quick_reply">"Respuesta rápida"</string>
</resources>

View file

@ -5,13 +5,11 @@
<string name="notification_channel_noisy">"Notifications bruyantes"</string>
<string name="notification_channel_silent">"Notifications silencieuses"</string>
<string name="notification_inline_reply_failed">"** Échec de lenvoi - veuillez ouvrir le salon"</string>
<string name="notification_invitation_action_join">"Rejoindre"</string>
<string name="notification_invitation_action_reject">"Rejeter"</string>
<string name="notification_invite_body">"Vous a invité(e) à discuter"</string>
<string name="notification_mentioned_you_body">"Mentionné(e): %1$s"</string>
<string name="notification_new_messages">"Nouveaux messages"</string>
<string name="notification_reaction_body">"A réagi avec %1$s"</string>
<string name="notification_room_action_mark_as_read">"Marquer comme lu"</string>
<string name="notification_room_invite_body">"Vous a invité(e) à rejoindre le salon"</string>
<string name="notification_sender_me">"Moi"</string>
<string name="notification_test_push_notification_content">"Vous êtes en train de voir la notification ! Cliquez-moi !"</string>
@ -49,5 +47,7 @@
<string name="push_distributor_firebase_android">"Services Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Aucun service Google Play valide na été trouvé. Les notifications peuvent ne pas fonctionner correctement."</string>
<string name="notification_fallback_content">"Notification"</string>
<string name="notification_invitation_action_join">"Rejoindre"</string>
<string name="notification_room_action_mark_as_read">"Marquer comme lu"</string>
<string name="notification_room_action_quick_reply">"Réponse rapide"</string>
</resources>

View file

@ -5,14 +5,12 @@
<string name="notification_channel_noisy">"Zajos értesítések"</string>
<string name="notification_channel_silent">"Csendes értesítések"</string>
<string name="notification_inline_reply_failed">"** Nem sikerült elküldeni kérlek nyisd meg a szobát"</string>
<string name="notification_invitation_action_join">"Csatlakozás"</string>
<string name="notification_invitation_action_reject">"Elutasítás"</string>
<string name="notification_invite_body">"Meghívta, hogy csevegjen"</string>
<string name="notification_mentioned_you_body">"Megemlítette Önt: %1$s"</string>
<string name="notification_new_messages">"Új üzenetek"</string>
<string name="notification_reaction_body">"Ezzel reagált: %1$s"</string>
<string name="notification_room_action_mark_as_read">"Megjelölés olvasottként"</string>
<string name="notification_room_invite_body">"Meghívta, hogy csatlakozzon a szobához"</string>
<string name="notification_room_invite_body">"Meghívott, hogy csatlakozz a szobához"</string>
<string name="notification_sender_me">"Én"</string>
<string name="notification_test_push_notification_content">"Az értesítést nézed! Kattints ide!"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
@ -49,5 +47,7 @@
<string name="push_distributor_firebase_android">"Google szolgáltatások"</string>
<string name="push_no_valid_google_play_services_apk_android">"A Google Play szolgáltatások nem találhatók. Előfordulhat, hogy az értesítések nem működnek megfelelően."</string>
<string name="notification_fallback_content">"Értesítés"</string>
<string name="notification_invitation_action_join">"Csatlakozás"</string>
<string name="notification_room_action_mark_as_read">"Megjelölés olvasottként"</string>
<string name="notification_room_action_quick_reply">"Gyors válasz"</string>
</resources>

View file

@ -5,13 +5,11 @@
<string name="notification_channel_noisy">"Pemberitahuan berisik"</string>
<string name="notification_channel_silent">"Pemberitahuan diam"</string>
<string name="notification_inline_reply_failed">"** Gagal mengirim — silakan buka ruangan"</string>
<string name="notification_invitation_action_join">"Bergabung"</string>
<string name="notification_invitation_action_reject">"Tolak"</string>
<string name="notification_invite_body">"Mengundang Anda untuk mengobrol"</string>
<string name="notification_mentioned_you_body">"Menyebutkan Anda: %1$s"</string>
<string name="notification_new_messages">"Pesan Baru"</string>
<string name="notification_reaction_body">"Menghapus dengan %1$s"</string>
<string name="notification_room_action_mark_as_read">"Tandai sebagai dibaca"</string>
<string name="notification_room_invite_body">"Mengundang Anda untuk bergabung ke ruangan"</string>
<string name="notification_sender_me">"Saya"</string>
<string name="notification_test_push_notification_content">"Anda sedang melihat pemberitahuan ini! Klik saya!"</string>
@ -43,5 +41,6 @@
<string name="push_distributor_firebase_android">"Layanan Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Tidak ditemukan Layanan Google Play yang valid. Pemberitahuan mungkin tidak berfungsi dengan baik."</string>
<string name="notification_fallback_content">"Notifikasi"</string>
<string name="notification_invitation_action_join">"Gabung"</string>
<string name="notification_room_action_quick_reply">"Balas cepat"</string>
</resources>

View file

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="notification_channel_call">"Chiamata"</string>
<string name="notification_channel_listening_for_events">"Ascolto degli eventi"</string>
<string name="notification_channel_noisy">"Notifiche con suono"</string>
<string name="notification_channel_silent">"Notifiche silenziose"</string>
<string name="notification_inline_reply_failed">"** Invio fallito - si prega di aprire la stanza"</string>
<string name="notification_invitation_action_join">"Entra"</string>
<string name="notification_invitation_action_reject">"Rifiuta"</string>
<string name="notification_invite_body">"Ti ha invitato a chattare"</string>
<string name="notification_mentioned_you_body">"Ti ha menzionato: %1$s"</string>
<string name="notification_new_messages">"Nuovi messaggi"</string>
<string name="notification_reaction_body">"Ha reagito con %1$s"</string>
<string name="notification_room_action_mark_as_read">"Segna come letto"</string>
<string name="notification_room_invite_body">"Ti ha invitato ad entrare nella stanza"</string>
<string name="notification_sender_me">"Io"</string>
<string name="notification_test_push_notification_content">"Stai visualizzando la notifica! Cliccami!"</string>
@ -47,5 +47,7 @@
<string name="push_distributor_firebase_android">"Servizi Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Google Play Services non trovato. Le notifiche non funzioneranno bene."</string>
<string name="notification_fallback_content">"Notifica"</string>
<string name="notification_invitation_action_join">"Entra"</string>
<string name="notification_room_action_mark_as_read">"Segna come letto"</string>
<string name="notification_room_action_quick_reply">"Risposta rapida"</string>
</resources>

View file

@ -5,12 +5,10 @@
<string name="notification_channel_noisy">"Notificări zgomotoase"</string>
<string name="notification_channel_silent">"Notificări silențioase"</string>
<string name="notification_inline_reply_failed">"** Trimiterea eșuată - vă rugăm să deschideți camera"</string>
<string name="notification_invitation_action_join">"Alăturați-vă"</string>
<string name="notification_invitation_action_reject">"Respingeți"</string>
<string name="notification_invite_body">"V-a invitat la o discuție"</string>
<string name="notification_new_messages">"Mesaje noi"</string>
<string name="notification_reaction_body">"A reacționat cu %1$s"</string>
<string name="notification_room_action_mark_as_read">"Marcați ca citit"</string>
<string name="notification_room_invite_body">"V-a invitat să vă alăturați camerei"</string>
<string name="notification_sender_me">"Eu"</string>
<string name="notification_test_push_notification_content">"Vizualizați o notificare! Faceți clic pe mine!"</string>

View file

@ -5,13 +5,11 @@
<string name="notification_channel_noisy">"Шумные уведомления"</string>
<string name="notification_channel_silent">"Бесшумные уведомления"</string>
<string name="notification_inline_reply_failed">"** Не удалось отправить - пожалуйста, откройте комнату"</string>
<string name="notification_invitation_action_join">"Присоединиться"</string>
<string name="notification_invitation_action_reject">"Отклонить"</string>
<string name="notification_invite_body">"Пригласил вас в чат"</string>
<string name="notification_mentioned_you_body">"Упомянул вас: %1$s"</string>
<string name="notification_new_messages">"Новые сообщения"</string>
<string name="notification_reaction_body">"Отреагировал на %1$s"</string>
<string name="notification_room_action_mark_as_read">"Отметить как прочитанное"</string>
<string name="notification_room_invite_body">"Пригласил вас в комнату"</string>
<string name="notification_sender_me">"Я"</string>
<string name="notification_test_push_notification_content">"Вы просматриваете уведомление! Нажмите на меня!"</string>
@ -55,5 +53,7 @@
<string name="push_distributor_firebase_android">"Сервисы Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Не найдены действующие службы Google Play. Уведомления могут работать некорректно."</string>
<string name="notification_fallback_content">"Уведомление"</string>
<string name="notification_invitation_action_join">"Присоединиться"</string>
<string name="notification_room_action_mark_as_read">"Пометить как прочитанное"</string>
<string name="notification_room_action_quick_reply">"Быстрый ответ"</string>
</resources>

View file

@ -5,13 +5,11 @@
<string name="notification_channel_noisy">"Hlasité oznámenia"</string>
<string name="notification_channel_silent">"Tiché oznámenia"</string>
<string name="notification_inline_reply_failed">"** Nepodarilo sa odoslať - prosím otvorte miestnosť"</string>
<string name="notification_invitation_action_join">"Pripojiť sa"</string>
<string name="notification_invitation_action_reject">"Zamietnuť"</string>
<string name="notification_invite_body">"Vás pozval/a na konverzáciu"</string>
<string name="notification_mentioned_you_body">"Spomenul/a vás: %1$s"</string>
<string name="notification_new_messages">"Nové správy"</string>
<string name="notification_reaction_body">"Reagoval/a s %1$s"</string>
<string name="notification_room_action_mark_as_read">"Označiť ako prečítané"</string>
<string name="notification_room_invite_body">"Vás pozval do miestnosti"</string>
<string name="notification_sender_me">"Ja"</string>
<string name="notification_test_push_notification_content">"Prezeráte si oznámenie! Kliknite na mňa!"</string>
@ -55,5 +53,7 @@
<string name="push_distributor_firebase_android">"Služby Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Nenašli sa žiadne platné služby Google Play. Oznámenia nemusia fungovať správne."</string>
<string name="notification_fallback_content">"Oznámenie"</string>
<string name="notification_invitation_action_join">"Pripojiť sa"</string>
<string name="notification_room_action_mark_as_read">"Označiť ako prečítané"</string>
<string name="notification_room_action_quick_reply">"Rýchla odpoveď"</string>
</resources>

View file

@ -3,13 +3,11 @@
<string name="notification_channel_call">"通話"</string>
<string name="notification_channel_silent">"無聲通知"</string>
<string name="notification_inline_reply_failed">"** 無法傳送,請開啟聊天室"</string>
<string name="notification_invitation_action_join">"加入"</string>
<string name="notification_invitation_action_reject">"拒絕"</string>
<string name="notification_invite_body">"邀請您聊天"</string>
<string name="notification_mentioned_you_body">"提及您:%1$s"</string>
<string name="notification_new_messages">"新訊息"</string>
<string name="notification_reaction_body">"回應 %1$s"</string>
<string name="notification_room_action_mark_as_read">"標示為已讀"</string>
<string name="notification_room_invite_body">"邀請您加入聊天室"</string>
<string name="notification_sender_me">"我"</string>
<string name="notification_test_push_notification_content">"您正在查看通知!點我!"</string>
@ -34,5 +32,6 @@
<string name="push_distributor_background_sync_android">"背景同步"</string>
<string name="push_distributor_firebase_android">"Google 服務"</string>
<string name="notification_fallback_content">"通知"</string>
<string name="notification_invitation_action_join">"加入"</string>
<string name="notification_room_action_quick_reply">"快速回覆"</string>
</resources>

View file

@ -5,13 +5,11 @@
<string name="notification_channel_noisy">"Noisy notifications"</string>
<string name="notification_channel_silent">"Silent notifications"</string>
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
<string name="notification_invitation_action_join">"Join"</string>
<string name="notification_invitation_action_reject">"Reject"</string>
<string name="notification_invite_body">"Invited you to chat"</string>
<string name="notification_mentioned_you_body">"Mentioned you: %1$s"</string>
<string name="notification_new_messages">"New Messages"</string>
<string name="notification_reaction_body">"Reacted with %1$s"</string>
<string name="notification_room_action_mark_as_read">"Mark as read"</string>
<string name="notification_room_invite_body">"Invited you to join the room"</string>
<string name="notification_sender_me">"Me"</string>
<string name="notification_test_push_notification_content">"You are viewing the notification! Click me!"</string>
@ -49,5 +47,7 @@
<string name="push_distributor_firebase_android">"Google Services"</string>
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>
<string name="notification_fallback_content">"Notification"</string>
<string name="notification_invitation_action_join">"Join"</string>
<string name="notification_room_action_mark_as_read">"Mark as read"</string>
<string name="notification_room_action_quick_reply">"Quick reply"</string>
</resources>

View file

@ -105,7 +105,6 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
*/
override fun onUnregistered(context: Context, instance: String) {
Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
TODO()
/*
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
pushDataStore.setFdroidSyncBackgroundMode(mode)

View file

@ -209,10 +209,9 @@ private fun RoomSummaryView(
summary: RoomSummaryDetails,
isSelected: Boolean,
onSelection: (RoomSummaryDetails) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
modifier = Modifier
.clickable { onSelection(summary) }
.fillMaxWidth()
.padding(start = 16.dp, end = 4.dp)

View file

@ -58,6 +58,11 @@ object TestTags {
*/
val richTextEditor = TestTag("rich_text_editor")
/**
* Message bubble.
*/
val messageBubble = TestTag("message_bubble")
/**
* Dialogs.
*/

View file

@ -405,14 +405,13 @@ private fun TextInput(
onError: (Throwable) -> Unit,
onTyping: (Boolean) -> Unit,
onRichContentSelected: ((Uri) -> Unit)?,
modifier: Modifier = Modifier,
) {
val bgColor = ElementTheme.colors.bgSubtleSecondary
val borderColor = ElementTheme.colors.borderDisabled
val roundedCorners = textInputRoundedCornerShape(composerMode = composerMode)
Column(
modifier = modifier
modifier = Modifier
.clip(roundedCorners)
.border(0.5.dp, borderColor, roundedCorners)
.background(color = bgColor)
@ -464,15 +463,14 @@ private fun TextInput(
private fun ComposerModeView(
composerMode: MessageComposerMode,
onResetComposerMode: () -> Unit,
modifier: Modifier = Modifier,
) {
when (composerMode) {
is MessageComposerMode.Edit -> {
EditingModeView(onResetComposerMode = onResetComposerMode, modifier = modifier)
EditingModeView(onResetComposerMode = onResetComposerMode)
}
is MessageComposerMode.Reply -> {
ReplyToModeView(
modifier = modifier.padding(8.dp),
modifier = Modifier.padding(8.dp),
senderName = composerMode.senderName,
text = composerMode.defaultContent,
attachmentThumbnailInfo = composerMode.attachmentThumbnailInfo,
@ -486,12 +484,11 @@ private fun ComposerModeView(
@Composable
private fun EditingModeView(
onResetComposerMode: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp)
) {
@ -881,11 +878,8 @@ internal fun TextComposerVoicePreview() = ElementPreview {
@Composable
private fun PreviewColumn(
items: ImmutableList<@Composable () -> Unit>,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
) {
Column {
items.forEach { item ->
Box(
modifier = Modifier.height(IntrinsicSize.Min)

View file

@ -43,6 +43,7 @@ import io.element.android.wysiwyg.view.models.LinkAction
import kotlinx.coroutines.launch
import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction
@Composable
internal fun TextFormatting(
state: RichTextEditorState,

View file

@ -124,11 +124,10 @@ private fun PlayerButton(
type: PlayerButtonType,
enabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
IconButton(
onClick = onClick,
modifier = modifier
modifier = Modifier
.background(color = ElementTheme.colors.bgCanvasDefault, shape = CircleShape)
.size(30.dp),
enabled = enabled,

View file

@ -88,9 +88,7 @@ internal fun VoiceMessageRecording(
}
@Composable
private fun RedRecordingDot(
modifier: Modifier = Modifier,
) {
private fun RedRecordingDot() {
val infiniteTransition = rememberInfiniteTransition("RedRecordingDot")
val alpha by infiniteTransition.animateFloat(
initialValue = 1f,
@ -102,7 +100,7 @@ private fun RedRecordingDot(
label = "RedRecordingDotAlpha",
)
Box(
modifier = modifier
modifier = Modifier
.size(8.dp)
.alpha(alpha)
.background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape)

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="rich_text_editor_bullet_list">"Пераключыць маркіраваны спіс"</string>
<string name="rich_text_editor_close_formatting_options">"Закрыць параметры фарматавання"</string>
<string name="rich_text_editor_code_block">"Пераключыць блок кода"</string>
<string name="rich_text_editor_composer_placeholder">"Паведамленне…"</string>
<string name="rich_text_editor_create_link">"Стварыце спасылку"</string>
<string name="rich_text_editor_edit_link">"Рэдагаваць спасылку"</string>
<string name="rich_text_editor_format_bold">"Ужыць тоўсты шрыфт"</string>
<string name="rich_text_editor_format_italic">"Ужыць курсіўны фармат"</string>
<string name="rich_text_editor_format_strikethrough">"Ужыць фармат закрэслівання"</string>
<string name="rich_text_editor_format_underline">"Ужыць фармат падкрэслення"</string>
<string name="rich_text_editor_full_screen_toggle">"Пераключэнне поўнаэкраннага рэжыму"</string>
<string name="rich_text_editor_indent">"Водступ"</string>
<string name="rich_text_editor_inline_code">"Ужыць убудаваны фармат кода"</string>
<string name="rich_text_editor_link">"Усталяваць спасылку"</string>
<string name="rich_text_editor_numbered_list">"Пераключыць нумараваны спіс"</string>
<string name="rich_text_editor_open_compose_options">"Адкрыйце параметры кампазіцыі"</string>
<string name="rich_text_editor_quote">"Пераключыць цытату"</string>
<string name="rich_text_editor_remove_link">"Выдаліць спасылку"</string>
<string name="rich_text_editor_unindent">"Без водступу"</string>
<string name="rich_text_editor_url_placeholder">"Спасылка"</string>
<string name="rich_text_editor_a11y_add_attachment">"Дадаць далучэнне"</string>
<string name="screen_room_voice_message_tooltip">"Утрымлівайце для запісу"</string>
</resources>

View file

@ -0,0 +1,249 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_delete">"Выдаліць"</string>
<string name="a11y_hide_password">"Схаваць пароль"</string>
<string name="a11y_jump_to_bottom">"Перайсці ўніз"</string>
<string name="a11y_notifications_mentions_only">"Толькі згадкі"</string>
<string name="a11y_notifications_muted">"Гук адключаны"</string>
<string name="a11y_page_n">"Старонка %1$d"</string>
<string name="a11y_pause">"Паўза"</string>
<string name="a11y_pin_field">"Поле PIN-кода"</string>
<string name="a11y_play">"Прайграць"</string>
<string name="a11y_poll">"Апытанне"</string>
<string name="a11y_poll_end">"Апытанне скончана"</string>
<string name="a11y_react_with">"Рэагаваць з %1$s"</string>
<string name="a11y_react_with_other_emojis">"Рэагаваць з іншымі эмодзі"</string>
<string name="a11y_read_receipts_multiple">"Прачытана %1$s і %2$s"</string>
<string name="a11y_read_receipts_single">"Прачытана %1$s"</string>
<string name="a11y_read_receipts_tap_to_show_all">"Націсніце, каб паказаць усе"</string>
<string name="a11y_remove_reaction_with">"Выдаліць рэакцыю з %1$s"</string>
<string name="a11y_send_files">"Адправіць файлы"</string>
<string name="a11y_show_password">"Паказаць пароль"</string>
<string name="a11y_start_call">"Пазваніць"</string>
<string name="a11y_user_menu">"Меню карыстальніка"</string>
<string name="a11y_voice_message_record">"Запісаць галасавое паведамленне."</string>
<string name="a11y_voice_message_stop_recording">"Спыніць запіс"</string>
<string name="action_accept">"Прыняць"</string>
<string name="action_add_to_timeline">"Дадаць на часовую шкалу"</string>
<string name="action_back">"Назад"</string>
<string name="action_cancel">"Скасаваць"</string>
<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_confirm">"Пацвердзіць"</string>
<string name="action_continue">"Працягнуць"</string>
<string name="action_copy">"Капіраваць"</string>
<string name="action_copy_link">"Скапіраваць спасылку"</string>
<string name="action_copy_link_to_message">"Скапіраваць спасылку на паведамленне"</string>
<string name="action_create">"Стварыць"</string>
<string name="action_create_a_room">"Стварыце пакой"</string>
<string name="action_decline">"Адхіліць"</string>
<string name="action_delete_poll">"Выдаліць апытанне"</string>
<string name="action_disable">"Адключыць"</string>
<string name="action_done">"Гатова"</string>
<string name="action_edit">"Рэдагаваць"</string>
<string name="action_edit_poll">"Рэдагаваць апытанне"</string>
<string name="action_enable">"Уключыць"</string>
<string name="action_end_poll">"Скончыць апытанне"</string>
<string name="action_enter_pin">"Увядзіце PIN-код"</string>
<string name="action_forgot_password">"Забылі пароль?"</string>
<string name="action_forward">"Пераслаць"</string>
<string name="action_invite">"Запрасіць"</string>
<string name="action_invite_friends">"Запрасіць карыстальникаў"</string>
<string name="action_invite_friends_to_app">"Запрасіць карыстальнікаў у %1$s"</string>
<string name="action_invite_people_to_app">"Запрасіць карыстальнікаў у %1$s"</string>
<string name="action_invites_list">"Запрашэнні"</string>
<string name="action_join">"Далучыцца"</string>
<string name="action_learn_more">"Падрабязней"</string>
<string name="action_leave">"Пакінуць"</string>
<string name="action_leave_conversation">"Пакінуць размову"</string>
<string name="action_leave_room">"Пакінуць пакой"</string>
<string name="action_manage_account">"Кіраванне ўліковым запісам"</string>
<string name="action_manage_devices">"Кіраванне прыладамі"</string>
<string name="action_next">"Далей"</string>
<string name="action_no">"Не"</string>
<string name="action_not_now">"Не зараз"</string>
<string name="action_ok">"Добра"</string>
<string name="action_open_settings">"Налады"</string>
<string name="action_open_with">"Адкрыць з дапамогай"</string>
<string name="action_quick_reply">"Хуткі адказ"</string>
<string name="action_quote">"Цытата"</string>
<string name="action_react">"Рэакцыя"</string>
<string name="action_remove">"Выдаліць"</string>
<string name="action_reply">"Адказаць"</string>
<string name="action_reply_in_thread">"Адказаць у гутаркі"</string>
<string name="action_report_bug">"Паведаміць пра памылку"</string>
<string name="action_report_content">"Паскардзіцца на змест"</string>
<string name="action_retry">"Паўтарыць"</string>
<string name="action_retry_decryption">"Паўтарыце расшыфроўку"</string>
<string name="action_save">"Захаваць"</string>
<string name="action_search">"Пошук"</string>
<string name="action_send">"Адправіць"</string>
<string name="action_send_message">"Адправіць паведамленне"</string>
<string name="action_share">"Падзяліцца"</string>
<string name="action_share_link">"Абагуліць спасылку"</string>
<string name="action_sign_in_again">"Увайдзіце яшчэ раз"</string>
<string name="action_signout">"Выйсці"</string>
<string name="action_signout_anyway">"Усё роўна выйсці"</string>
<string name="action_skip">"Прапусціць"</string>
<string name="action_start">"Пачаць"</string>
<string name="action_start_chat">"Пачаць чат"</string>
<string name="action_start_verification">"Пачаць праверку"</string>
<string name="action_static_map_load">"Націсніце, каб загрузіць карту"</string>
<string name="action_take_photo">"Зрабіць фота"</string>
<string name="action_tap_for_options">"Дакраніцеся, каб убачыць параметры"</string>
<string name="action_try_again">"Паўтарыць спробу"</string>
<string name="action_view_source">"Паказаць крыніцу"</string>
<string name="action_yes">"Так"</string>
<string name="action_load_more">"Загрузіць больш"</string>
<string name="common_about">"Аб праграме"</string>
<string name="common_acceptable_use_policy">"Палітыка дапушчальнага выкарыстання"</string>
<string name="common_advanced_settings">"Пашыраныя налады"</string>
<string name="common_analytics">"Аналітыка"</string>
<string name="common_appearance">"Знешні выгляд"</string>
<string name="common_audio">"Аўдыё"</string>
<string name="common_bubbles">"Бурбалкі"</string>
<string name="common_chat_backup">"Рэзервовае капіраванне чата"</string>
<string name="common_copyright">"Аўтарскае права"</string>
<string name="common_creating_room">"Стварэнне пакоя…"</string>
<string name="common_current_user_left_room">"Выйшаў з пакоя"</string>
<string name="common_dark">"Цёмная"</string>
<string name="common_decryption_error">"Памылка расшыфроўкі"</string>
<string name="common_developer_options">"Параметры распрацоўшчыка"</string>
<string name="common_direct_chat">"Прамы чат"</string>
<string name="common_edited_suffix">"(Адрэдагавана)"</string>
<string name="common_editing">"Рэдагаванне"</string>
<string name="common_emote">"* %1$s %2$s"</string>
<string name="common_encryption_enabled">"Шыфраванне ўключана"</string>
<string name="common_enter_your_pin">"Увядзіце свой PIN-код"</string>
<string name="common_error">"Памылка"</string>
<string name="common_everyone">"Усе"</string>
<string name="common_file">"Файл"</string>
<string name="common_file_saved_on_disk_android">"Файл захаваны ў папку Спампоўкі"</string>
<string name="common_forward_message">"Перасылка паведамлення"</string>
<string name="common_gif">"GIF"</string>
<string name="common_image">"Выява"</string>
<string name="common_in_reply_to">"У адказ на %1$s"</string>
<string name="common_install_apk_android">"Усталяваць APK"</string>
<string name="common_invite_unknown_profile">"Гэты Matrix ID не знойдзены, таму запрашэнне можа быць не атрымана."</string>
<string name="common_leaving_room">"Пакінуць пакой"</string>
<string name="common_light">"Светлая"</string>
<string name="common_link_copied_to_clipboard">"Спасылка скапіравана ў буфер абмену"</string>
<string name="common_loading">"Загрузка…"</string>
<string name="common_message">"Паведамленне"</string>
<string name="common_message_actions">"Дзеянні з паведамленням"</string>
<string name="common_message_removed">"Паведамленне выдалена"</string>
<string name="common_modern">"Сучасны"</string>
<string name="common_mute">"Адключыць гук"</string>
<string name="common_no_results">"Вынікаў няма"</string>
<string name="common_offline">"Па-за сеткай"</string>
<string name="common_password">"Пароль"</string>
<string name="common_people">"Людзі"</string>
<string name="common_permalink">"Пастаянная спасылка"</string>
<string name="common_permission">"Дазвол"</string>
<string name="common_poll_total_votes">"Усяго галасоў: %1$s"</string>
<string name="common_poll_undisclosed_text">"Вынікі будуць паказаны пасля завяршэння апытання"</string>
<string name="common_privacy_policy">"Палітыка прыватнасці"</string>
<string name="common_reaction">"Рэакцыя"</string>
<string name="common_reactions">"Рэакцыі"</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>
<string name="common_report_a_problem">"Паведаміць аб праблеме"</string>
<string name="common_report_submitted">"Скарга прынята"</string>
<string name="common_rich_text_editor">"Рэдактар фарматаванага тэксту"</string>
<string name="common_room">"Пакой"</string>
<string name="common_room_name">"Назва пакоя"</string>
<string name="common_room_name_placeholder">"напрыклад, назва вашага праекта"</string>
<string name="common_screen_lock">"Блакіроўка экрана"</string>
<string name="common_search_for_someone">"Шукаць карыстальніка"</string>
<string name="common_search_results">"Вынікі пошуку"</string>
<string name="common_security">"Бяспека"</string>
<string name="common_seen_by">"Прагледжана"</string>
<string name="common_sending">"Адпраўка…"</string>
<string name="common_sending_failed">"Памылка адпраўкі"</string>
<string name="common_sent">"Адпраўлена"</string>
<string name="common_server_not_supported">"Сервер не падтрымліваецца"</string>
<string name="common_server_url">"URL-адрас сервера"</string>
<string name="common_settings">"Налады"</string>
<string name="common_signing_out">"Выхад"</string>
<string name="common_starting_chat">"Пачатак чата…"</string>
<string name="common_sticker">"Стыкер"</string>
<string name="common_success">"Поспех"</string>
<string name="common_suggestions">"Прапановы"</string>
<string name="common_syncing">"Сінхранізацыя"</string>
<string name="common_system">"Сістэма"</string>
<string name="common_text">"Тэкст"</string>
<string name="common_third_party_notices">"Паведамленні трэціх асоб"</string>
<string name="common_thread">"Гутарка"</string>
<string name="common_topic">"Тэма"</string>
<string name="common_topic_placeholder">"Пра што гэты пакой?"</string>
<string name="common_unable_to_decrypt">"Немагчыма расшыфраваць"</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_video">"Відэа"</string>
<string name="common_voice_message">"Галасавое паведамленне"</string>
<string name="common_waiting">"Чакаем…"</string>
<string name="common_waiting_for_decryption_key">"Чакаю гэтага паведамлення"</string>
<string name="common_poll_end_confirmation">"Вы ўпэўнены, што хочаце скончыць гэтае апытанне?"</string>
<string name="common_poll_summary">"Апытанне: %1$s"</string>
<string name="common_verify_device">"Праверце прыладу"</string>
<string name="dialog_title_confirmation">"Пацвярджэнне"</string>
<string name="dialog_title_warning">"Папярэджанне"</string>
<string name="error_failed_creating_the_permalink">"Не атрымалася стварыць пастаянную спасылку"</string>
<string name="error_failed_loading_map">"%1$s не атрымалася загрузіць карту. Калі ласка паспрабуйце зноў пазней."</string>
<string name="error_failed_loading_messages">"Не ўдалося загрузіць паведамленні"</string>
<string name="error_failed_locating_user">"%1$s не магчыма атрымаць доступ да вашага месцазнаходжання. Калі ласка паспрабуйце зноў пазней."</string>
<string name="error_failed_uploading_voice_message">"Не ўдалося загрузіць ваша галасавое паведамленне."</string>
<string name="error_missing_location_auth_android">"У %1$s няма дазволу на доступ да вашага месцазнаходжання. Вы можаце даць доступ у Наладах."</string>
<string name="error_missing_location_rationale_android">"У %1$s няма дазволу на доступ да вашага месцазнаходжання. Дазвольце доступ ніжэй."</string>
<string name="error_missing_microphone_voice_rationale_android">"%1$s не мае дазволу на доступ да вашага мікрафона. Дазвольце доступ да запісу галасавога паведамлення."</string>
<string name="error_some_messages_have_not_been_sent">"Некаторыя паведамленні не былі адпраўлены"</string>
<string name="error_unknown">"На жаль, адбылася памылка"</string>
<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>
<plurals name="a11y_digits_entered">
<item quantity="one">"Уведзеная лічба %1$d"</item>
<item quantity="few">"Уведзена %1$d лічбы"</item>
<item quantity="many">"Уведзена %1$d лічб"</item>
</plurals>
<plurals name="a11y_read_receipts_multiple_with_others">
<item quantity="one">"Прачытана %1$s і %2$d іншым"</item>
<item quantity="few">"Прачытана %1$s і %2$d іншымі"</item>
<item quantity="many">"Прачытана %1$s і %2$d іншымі"</item>
</plurals>
<plurals name="common_member_count">
<item quantity="one">"%1$d карыстальнік"</item>
<item quantity="few">"%1$d карыстальнікаў"</item>
<item quantity="many">"%1$d карыстальнікаў"</item>
</plurals>
<plurals name="common_poll_votes_count">
<item quantity="one">"%d голас"</item>
<item quantity="few">"%d галасоў"</item>
<item quantity="many">"%d галасоў"</item>
</plurals>
<string name="preference_rageshake">"Rageshake паведаміць пра памылку"</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>
<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>
<string name="screen_share_open_google_maps">"Адкрыць у Google Maps"</string>
<string name="screen_share_open_osm_maps">"Адкрыць у OpenStreetMap"</string>
<string name="screen_share_this_location_action">"Падзяліцеся гэтым месцазнаходжаннем"</string>
<string name="screen_view_location_title">"Месцазнаходжанне"</string>
<string name="settings_version_number">"Версія: %1$s (%2$s)"</string>
<string name="test_language_identifier">"be"</string>
<string name="dialog_title_error">"Памылка"</string>
<string name="dialog_title_success">"Поспех"</string>
</resources>

View file

@ -119,6 +119,7 @@
<string name="common_enter_your_pin">"Zadejte svůj PIN"</string>
<string name="common_error">"Chyba"</string>
<string name="common_everyone">"Všichni"</string>
<string name="common_favourite">"Oblíbené"</string>
<string name="common_file">"Soubor"</string>
<string name="common_file_saved_on_disk_android">"Soubor byl uložen do složky Stažené soubory"</string>
<string name="common_forward_message">"Přeposlat zprávu"</string>

View file

@ -57,6 +57,7 @@
<string name="action_join">"Beitreten"</string>
<string name="action_learn_more">"Mehr erfahren"</string>
<string name="action_leave">"Verlassen"</string>
<string name="action_leave_conversation">"Unterhaltung verlassen"</string>
<string name="action_leave_room">"Raum verlassen"</string>
<string name="action_manage_account">"Konto verwalten"</string>
<string name="action_manage_devices">"Geräte verwalten"</string>
@ -118,6 +119,7 @@
<string name="common_enter_your_pin">"PIN eingeben"</string>
<string name="common_error">"Fehler"</string>
<string name="common_everyone">"Alle"</string>
<string name="common_favourite">"Favorit"</string>
<string name="common_file">"Datei"</string>
<string name="common_file_saved_on_disk_android">"Datei wurde unter Downloads gespeichert"</string>
<string name="common_forward_message">"Nachricht weiterleiten"</string>

View file

@ -119,6 +119,7 @@
<string name="common_enter_your_pin">"Saisissez votre code PIN"</string>
<string name="common_error">"Erreur"</string>
<string name="common_everyone">"Tout le monde"</string>
<string name="common_favourite">"Favori"</string>
<string name="common_file">"Fichier"</string>
<string name="common_file_saved_on_disk_android">"Fichier enregistré dans Téléchargements"</string>
<string name="common_forward_message">"Transférer le message"</string>

View file

@ -46,7 +46,7 @@
<string name="action_edit_poll">"Szavazás szerkesztése"</string>
<string name="action_enable">"Engedélyezés"</string>
<string name="action_end_poll">"Szavazás lezárása"</string>
<string name="action_enter_pin">"Adja meg a PIN-kódot"</string>
<string name="action_enter_pin">"Add meg a PIN-kódot"</string>
<string name="action_forgot_password">"Elfelejtetted a jelszavadat?"</string>
<string name="action_forward">"Tovább"</string>
<string name="action_invite">"Meghívás"</string>
@ -57,6 +57,7 @@
<string name="action_join">"Csatlakozás"</string>
<string name="action_learn_more">"További tudnivalók"</string>
<string name="action_leave">"Elhagyás"</string>
<string name="action_leave_conversation">"Beszélgetés elhagyása"</string>
<string name="action_leave_room">"Szoba elhagyása"</string>
<string name="action_manage_account">"Fiók kezelése"</string>
<string name="action_manage_devices">"Eszközök kezelése"</string>
@ -91,7 +92,7 @@
<string name="action_start_verification">"Ellenőrzés elindítása"</string>
<string name="action_static_map_load">"Koppints a térkép betöltéséhez"</string>
<string name="action_take_photo">"Fénykép készítése"</string>
<string name="action_tap_for_options">"Koppintson a lehetőségekért"</string>
<string name="action_tap_for_options">"Koppints a beállításokért"</string>
<string name="action_try_again">"Próbáld újra"</string>
<string name="action_view_source">"Forrás megtekintése"</string>
<string name="action_yes">"Igen"</string>
@ -118,6 +119,7 @@
<string name="common_enter_your_pin">"Add meg a PIN-kódodat"</string>
<string name="common_error">"Hiba"</string>
<string name="common_everyone">"Mindenki"</string>
<string name="common_favourite">"Kedvenc"</string>
<string name="common_file">"Fájl"</string>
<string name="common_file_saved_on_disk_android">"A fájl a Letöltések mappába mentve"</string>
<string name="common_forward_message">"Üzenet továbbítása"</string>
@ -200,13 +202,13 @@
<string name="dialog_title_confirmation">"Megerősítés"</string>
<string name="dialog_title_warning">"Figyelmeztetés"</string>
<string name="error_failed_creating_the_permalink">"Nem sikerült létrehozni az állandó hivatkozást"</string>
<string name="error_failed_loading_map">"Az %1$s nem tudta betölteni a térképet. Próbálja meg újra később."</string>
<string name="error_failed_loading_map">"A(z) %1$s nem tudta betölteni a térképet. Próbáld meg újra később."</string>
<string name="error_failed_loading_messages">"Nem sikerült betölteni az üzeneteket"</string>
<string name="error_failed_locating_user">"A(z) %1$s nem tudta elérni a tartózkodási helyét. Próbáld meg újra később."</string>
<string name="error_failed_uploading_voice_message">"Nem sikerült feltölteni a hangüzenetét."</string>
<string name="error_missing_location_auth_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyedhez. Ezt a beállításokban engedélyezheted."</string>
<string name="error_missing_location_rationale_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyéhez. Engedélyezze alább az elérését."</string>
<string name="error_missing_microphone_voice_rationale_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a mikrofonjához. Engedélyezze, hogy tudjon hangüzenetet felvenni."</string>
<string name="error_missing_microphone_voice_rationale_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a mikrofonhoz. Engedélyezd, hogy tudjon hangüzenetet felvenni."</string>
<string name="error_some_messages_have_not_been_sent">"Néhány üzenet nem került elküldésre"</string>
<string name="error_unknown">"Elnézést, hiba történt"</string>
<string name="invite_friends_rich_title">"🔐️ Csatlakozz hozzám itt: %1$s"</string>

View file

@ -119,6 +119,7 @@
<string name="common_enter_your_pin">"Inserisci il PIN"</string>
<string name="common_error">"Errore"</string>
<string name="common_everyone">"Tutti"</string>
<string name="common_favourite">"Preferiti"</string>
<string name="common_file">"File"</string>
<string name="common_file_saved_on_disk_android">"File salvato in Download"</string>
<string name="common_forward_message">"Inoltra messaggio"</string>

View file

@ -119,6 +119,7 @@
<string name="common_enter_your_pin">"Введите свой PIN-код"</string>
<string name="common_error">"Ошибка"</string>
<string name="common_everyone">"Для всех"</string>
<string name="common_favourite">"Избранное"</string>
<string name="common_file">"Файл"</string>
<string name="common_file_saved_on_disk_android">"Файл сохранен в «Загрузки»"</string>
<string name="common_forward_message">"Переслать сообщение"</string>

View file

@ -119,6 +119,7 @@
<string name="common_enter_your_pin">"Zadajte svoj PIN"</string>
<string name="common_error">"Chyba"</string>
<string name="common_everyone">"Všetci"</string>
<string name="common_favourite">"Obľúbené"</string>
<string name="common_file">"Súbor"</string>
<string name="common_file_saved_on_disk_android">"Súbor bol uložený do priečinka Stiahnuté súbory"</string>
<string name="common_forward_message">"Preposlať správu"</string>

View file

@ -119,6 +119,7 @@
<string name="common_enter_your_pin">"Enter your PIN"</string>
<string name="common_error">"Error"</string>
<string name="common_everyone">"Everyone"</string>
<string name="common_favourite">"Favourite"</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>