Merge branch 'develop' into renovate/org.maplibre.gl-android-sdk-11.x

This commit is contained in:
ganfra 2024-05-29 15:43:25 +02:00 committed by GitHub
commit 232c8de702
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1249 changed files with 18041 additions and 8127 deletions

View file

@ -28,6 +28,7 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.RequiresApi
import androidx.core.content.pm.PackageInfoCompat
import io.element.android.libraries.androidutils.R
import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
import io.element.android.libraries.core.mimetype.MimeTypes
@ -47,6 +48,19 @@ fun Context.getApplicationLabel(packageName: String): String {
}
}
/**
* Retrieve the versionCode from the Manifest.
* The value is more accurate than BuildConfig.VERSION_CODE, as it is correct according to the
* computation in the `androidComponents` block of the app build.gradle.kts file.
* In other words, the last digit (for the architecture) will be set, whereas BuildConfig.VERSION_CODE
* last digit will always be 0.
*/
fun Context.getVersionCodeFromManifest(): Long {
return PackageInfoCompat.getLongVersionCode(
packageManager.getPackageInfo(packageName, 0)
)
}
// ==============================================================================================================
// Clipboard helper
// ==============================================================================================================

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

@ -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">"Nenhuma aplicação encontrada capaz de continuar esta ação."</string>
</resources>

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

@ -86,6 +86,8 @@ sealed interface AsyncAction<out T> {
fun isFailure(): Boolean = this is Failure
fun isSuccess(): Boolean = this is Success
fun isReady() = isSuccess() || isFailure()
}
suspend inline fun <T> MutableState<AsyncAction<T>>.runCatchingUpdatingState(

View file

@ -91,6 +91,8 @@ sealed interface AsyncData<out T> {
fun isSuccess(): Boolean = this is Success<T>
fun isUninitialized(): Boolean = this == Uninitialized
fun isReady() = isSuccess() || isFailure()
}
suspend inline fun <T> MutableState<AsyncData<T>>.runCatchingUpdatingState(

View file

@ -25,7 +25,7 @@ data class BuildMeta(
val applicationId: String,
val lowPrivacyLoggingEnabled: Boolean,
val versionName: String,
val versionCode: Int,
val versionCode: Long,
val gitRevision: String,
val gitBranchName: String,
val flavorDescription: String,

View file

@ -45,8 +45,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun DialogLikeBannerMolecule(
title: String,
content: String,
onSubmitClicked: () -> Unit,
onDismissClicked: (() -> Unit)?,
onSubmitClick: () -> Unit,
onDismissClick: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
@ -68,9 +68,9 @@ fun DialogLikeBannerMolecule(
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Start,
)
if (onDismissClicked != null) {
if (onDismissClick != null) {
Icon(
modifier = Modifier.clickable(onClick = onDismissClicked),
modifier = Modifier.clickable(onClick = onDismissClick),
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_close)
)
@ -86,7 +86,7 @@ fun DialogLikeBannerMolecule(
text = stringResource(CommonStrings.action_continue),
size = ButtonSize.Medium,
modifier = Modifier.fillMaxWidth(),
onClick = onSubmitClicked,
onClick = onSubmitClick,
)
}
}
@ -99,7 +99,7 @@ internal fun DialogLikeBannerMoleculePreview() = ElementPreview {
DialogLikeBannerMolecule(
title = "Title",
content = "Content",
onSubmitClicked = {},
onDismissClicked = {}
onSubmitClick = {},
onDismissClick = {}
)
}

View file

@ -41,7 +41,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
/**
* A Page with:
* - a top bar as TobAppBar with optional back button (displayed if [onBackClicked] is not null)
* - a top bar as TobAppBar with optional back button (displayed if [onBackClick] is not null)
* - a header, as IconTitleSubtitleMolecule
* - a content.
* - a footer, as ButtonColumnMolecule
@ -52,21 +52,21 @@ fun FlowStepPage(
iconVector: ImageVector?,
title: String,
modifier: Modifier = Modifier,
onBackClicked: (() -> Unit)? = null,
onBackClick: (() -> Unit)? = null,
subTitle: String? = null,
content: @Composable () -> Unit = {},
buttons: @Composable ColumnScope.() -> Unit = {},
content: @Composable () -> Unit = {},
) {
BackHandler(enabled = onBackClicked != null) {
onBackClicked?.invoke()
BackHandler(enabled = onBackClick != null) {
onBackClick?.invoke()
}
HeaderFooterPage(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = {
if (onBackClicked != null) {
BackButton(onClick = onBackClicked)
if (onBackClick != null) {
BackButton(onClick = onBackClick)
}
},
title = {},
@ -94,25 +94,24 @@ fun FlowStepPage(
@Composable
internal fun FlowStepPagePreview() = ElementPreview {
FlowStepPage(
onBackClicked = {},
onBackClick = {},
title = "Title",
subTitle = "Subtitle",
iconVector = CompoundIcons.Computer(),
content = {
Box(
Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Content",
style = ElementTheme.typography.fontHeadingXlBold
)
}
},
buttons = {
TextButton(text = "A button", onClick = { })
Button(text = "Continue", onClick = { })
}
)
) {
Box(
Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Content",
style = ElementTheme.typography.fontHeadingXlBold
)
}
}
}

View file

@ -168,6 +168,7 @@ data class BloomLayer(
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
* @param alpha The alpha value to apply to the bloom effect.
*/
@SuppressWarnings("ModifierComposed")
fun Modifier.bloom(
hash: String?,
background: Color,
@ -312,6 +313,7 @@ fun Modifier.bloom(
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
* @param alpha The alpha value to apply to the bloom effect.
*/
@SuppressWarnings("ModifierComposed")
fun Modifier.avatarBloom(
avatarData: AvatarData,
background: Color,

View file

@ -67,7 +67,7 @@ fun ProgressDialog(
modifier = modifier,
text = text,
isCancellable = isCancellable,
onCancelClicked = onDismissRequest,
onCancelClick = onDismissRequest,
progressIndicator = {
when (type) {
is ProgressDialogType.Indeterminate -> {
@ -98,7 +98,7 @@ private fun ProgressDialogContent(
modifier: Modifier = Modifier,
text: String? = null,
isCancellable: Boolean = false,
onCancelClicked: () -> Unit = {},
onCancelClick: () -> Unit = {},
progressIndicator: @Composable () -> Unit = {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary
@ -133,7 +133,7 @@ private fun ProgressDialogContent(
) {
TextButton(
text = stringResource(id = CommonStrings.action_cancel),
onClick = onCancelClicked,
onClick = onCancelClick,
)
}
}

View file

@ -99,7 +99,7 @@ internal fun AsyncActionViewPreview(
ConfirmationDialog(
title = "Confirmation",
content = "Are you sure?",
onSubmitClicked = {},
onSubmitClick = {},
onDismiss = {},
)
},

View file

@ -34,7 +34,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun ConfirmationDialog(
content: String,
onSubmitClicked: () -> Unit,
onSubmitClick: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
@ -42,8 +42,8 @@ fun ConfirmationDialog(
cancelText: String = stringResource(id = CommonStrings.action_cancel),
destructiveSubmit: Boolean = false,
thirdButtonText: String? = null,
onCancelClicked: () -> Unit = onDismiss,
onThirdButtonClicked: () -> Unit = {},
onCancelClick: () -> Unit = onDismiss,
onThirdButtonClick: () -> Unit = {},
) {
BasicAlertDialog(modifier = modifier, onDismissRequest = onDismiss) {
ConfirmationDialogContent(
@ -53,9 +53,9 @@ fun ConfirmationDialog(
cancelText = cancelText,
thirdButtonText = thirdButtonText,
destructiveSubmit = destructiveSubmit,
onSubmitClicked = onSubmitClicked,
onCancelClicked = onCancelClicked,
onThirdButtonClicked = onThirdButtonClicked,
onSubmitClick = onSubmitClick,
onCancelClick = onCancelClick,
onThirdButtonClick = onThirdButtonClick,
)
}
}
@ -65,11 +65,11 @@ private fun ConfirmationDialogContent(
content: String,
submitText: String,
cancelText: String,
onSubmitClicked: () -> Unit,
onCancelClicked: () -> Unit,
onSubmitClick: () -> Unit,
onCancelClick: () -> Unit,
title: String? = null,
thirdButtonText: String? = null,
onThirdButtonClicked: () -> Unit = {},
onThirdButtonClick: () -> Unit = {},
destructiveSubmit: Boolean = false,
icon: @Composable (() -> Unit)? = null,
) {
@ -77,11 +77,11 @@ private fun ConfirmationDialogContent(
title = title,
content = content,
submitText = submitText,
onSubmitClicked = onSubmitClicked,
onSubmitClick = onSubmitClick,
cancelText = cancelText,
onCancelClicked = onCancelClicked,
onCancelClick = onCancelClick,
thirdButtonText = thirdButtonText,
onThirdButtonClicked = onThirdButtonClicked,
onThirdButtonClick = onThirdButtonClick,
destructiveSubmit = destructiveSubmit,
icon = icon,
)
@ -98,9 +98,9 @@ internal fun ConfirmationDialogContentPreview() =
submitText = "OK",
cancelText = "Cancel",
thirdButtonText = "Disable",
onSubmitClicked = {},
onCancelClicked = {},
onThirdButtonClicked = {},
onSubmitClick = {},
onCancelClick = {},
onThirdButtonClick = {},
)
}
}
@ -114,7 +114,7 @@ internal fun ConfirmationDialogPreview() = ElementPreview {
submitText = "OK",
cancelText = "Cancel",
thirdButtonText = "Disable",
onSubmitClicked = {},
onSubmitClick = {},
onDismiss = {}
)
}

View file

@ -44,7 +44,7 @@ fun ErrorDialog(
title = title,
content = content,
submitText = submitText,
onSubmitClicked = onDismiss,
onSubmitClick = onDismiss,
)
}
}
@ -52,7 +52,7 @@ fun ErrorDialog(
@Composable
private fun ErrorDialogContent(
content: String,
onSubmitClicked: () -> Unit,
onSubmitClick: () -> Unit,
title: String = ErrorDialogDefaults.title,
submitText: String = ErrorDialogDefaults.submitText,
) {
@ -60,7 +60,7 @@ private fun ErrorDialogContent(
title = title,
content = content,
submitText = submitText,
onSubmitClicked = onSubmitClicked,
onSubmitClick = onSubmitClick,
)
}
@ -76,7 +76,7 @@ internal fun ErrorDialogContentPreview() {
DialogPreview {
ErrorDialogContent(
content = "Content",
onSubmitClicked = {},
onSubmitClick = {},
)
}
}

View file

@ -67,7 +67,7 @@ fun ListDialog(
cancelText = cancelText,
submitText = submitText,
onDismissRequest = onDismissRequest,
onSubmitClicked = onSubmit,
onSubmitClick = onSubmit,
enabled = enabled,
listItems = listItems,
)
@ -78,7 +78,7 @@ fun ListDialog(
private fun ListDialogContent(
listItems: LazyListScope.() -> Unit,
onDismissRequest: () -> Unit,
onSubmitClicked: () -> Unit,
onSubmitClick: () -> Unit,
cancelText: String,
submitText: String,
title: String? = null,
@ -90,8 +90,8 @@ private fun ListDialogContent(
subtitle = subtitle,
cancelText = cancelText,
submitText = submitText,
onCancelClicked = onDismissRequest,
onSubmitClicked = onSubmitClicked,
onCancelClick = onDismissRequest,
onSubmitClick = onSubmitClick,
enabled = enabled,
applyPaddingToContents = false,
) {
@ -109,15 +109,15 @@ internal fun ListDialogContentPreview() {
ListDialogContent(
listItems = {
item {
TextFieldListItem(placeholder = "Text input", text = "", onTextChanged = {})
TextFieldListItem(placeholder = "Text input", text = "", onTextChange = {})
}
item {
TextFieldListItem(placeholder = "Another text input", text = "", onTextChanged = {})
TextFieldListItem(placeholder = "Another text input", text = "", onTextChange = {})
}
},
title = "Dialog title",
onDismissRequest = {},
onSubmitClicked = {},
onSubmitClick = {},
cancelText = "Cancel",
submitText = "Save",
)
@ -131,10 +131,10 @@ internal fun ListDialogPreview() = ElementPreview {
ListDialog(
listItems = {
item {
TextFieldListItem(placeholder = "Text input", text = "", onTextChanged = {})
TextFieldListItem(placeholder = "Text input", text = "", onTextChange = {})
}
item {
TextFieldListItem(placeholder = "Another text input", text = "", onTextChanged = {})
TextFieldListItem(placeholder = "Another text input", text = "", onTextChange = {})
}
},
title = "Dialog title",

View file

@ -44,7 +44,7 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
fun MultipleSelectionDialog(
options: ImmutableList<ListOption>,
onConfirmClicked: (List<Int>) -> Unit,
onConfirmClick: (List<Int>) -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
confirmButtonTitle: String = stringResource(CommonStrings.action_confirm),
@ -70,7 +70,7 @@ fun MultipleSelectionDialog(
subtitle = decoratedSubtitle,
options = options,
confirmButtonTitle = confirmButtonTitle,
onConfirmClicked = onConfirmClicked,
onConfirmClick = onConfirmClick,
dismissButtonTitle = dismissButtonTitle,
onDismissRequest = onDismissRequest,
initialSelected = initialSelection,
@ -82,7 +82,7 @@ fun MultipleSelectionDialog(
private fun MultipleSelectionDialogContent(
options: ImmutableList<ListOption>,
confirmButtonTitle: String,
onConfirmClicked: (List<Int>) -> Unit,
onConfirmClick: (List<Int>) -> Unit,
dismissButtonTitle: String,
onDismissRequest: () -> Unit,
title: String? = null,
@ -97,11 +97,11 @@ private fun MultipleSelectionDialogContent(
title = title,
subtitle = subtitle,
submitText = confirmButtonTitle,
onSubmitClicked = {
onConfirmClicked(selectedOptionIndexes.toList())
onSubmitClick = {
onConfirmClick(selectedOptionIndexes.toList())
},
cancelText = dismissButtonTitle,
onCancelClicked = onDismissRequest,
onCancelClick = onDismissRequest,
applyPaddingToContents = false,
) {
LazyColumn {
@ -138,7 +138,7 @@ internal fun MultipleSelectionDialogContentPreview() {
MultipleSelectionDialogContent(
title = "Dialog title",
options = options,
onConfirmClicked = {},
onConfirmClick = {},
onDismissRequest = {},
confirmButtonTitle = "Save",
dismissButtonTitle = "Cancel",
@ -159,7 +159,7 @@ internal fun MultipleSelectionDialogPreview() = ElementPreview {
MultipleSelectionDialog(
title = "Dialog title",
options = options,
onConfirmClicked = {},
onConfirmClick = {},
onDismissRequest = {},
confirmButtonTitle = "Save",
dismissButtonTitle = "Cancel",

View file

@ -66,9 +66,9 @@ private fun RetryDialogContent(
title = title,
content = content,
submitText = retryText,
onSubmitClicked = onRetry,
onSubmitClick = onRetry,
cancelText = dismissText,
onCancelClicked = onDismiss,
onCancelClick = onDismiss,
)
}

View file

@ -41,7 +41,7 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
fun SingleSelectionDialog(
options: ImmutableList<ListOption>,
onOptionSelected: (Int) -> Unit,
onSelectOption: (Int) -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
@ -65,7 +65,7 @@ fun SingleSelectionDialog(
title = title,
subtitle = decoratedSubtitle,
options = options,
onOptionSelected = onOptionSelected,
onOptionClick = onSelectOption,
dismissButtonTitle = dismissButtonTitle,
onDismissRequest = onDismissRequest,
initialSelection = initialSelection,
@ -76,7 +76,7 @@ fun SingleSelectionDialog(
@Composable
private fun SingleSelectionDialogContent(
options: ImmutableList<ListOption>,
onOptionSelected: (Int) -> Unit,
onOptionClick: (Int) -> Unit,
dismissButtonTitle: String,
onDismissRequest: () -> Unit,
title: String? = null,
@ -87,7 +87,7 @@ private fun SingleSelectionDialogContent(
title = title,
subtitle = subtitle,
submitText = dismissButtonTitle,
onSubmitClicked = onDismissRequest,
onSubmitClick = onDismissRequest,
applyPaddingToContents = false,
) {
LazyColumn {
@ -96,7 +96,7 @@ private fun SingleSelectionDialogContent(
headline = option.title,
supportingText = option.subtitle,
selected = index == initialSelection,
onSelected = { onOptionSelected(index) },
onSelect = { onOptionClick(index) },
compactLayout = true,
modifier = Modifier.padding(start = 8.dp)
)
@ -118,7 +118,7 @@ internal fun SingleSelectionDialogContentPreview() {
SingleSelectionDialogContent(
title = "Dialog title",
options = options,
onOptionSelected = {},
onOptionClick = {},
onDismissRequest = {},
dismissButtonTitle = "Cancel",
initialSelection = 0
@ -138,7 +138,7 @@ internal fun SingleSelectionDialogPreview() = ElementPreview {
SingleSelectionDialog(
title = "Dialog title",
options = options,
onOptionSelected = {},
onSelectOption = {},
onDismissRequest = {},
dismissButtonTitle = "Cancel",
initialSelection = 0

View file

@ -41,7 +41,7 @@ import kotlinx.collections.immutable.toImmutableList
fun MultipleSelectionListItem(
headline: String,
options: ImmutableList<ListOption>,
onSelectionChanged: (List<Int>) -> Unit,
onSelectionChange: (List<Int>) -> Unit,
resultFormatter: (List<Int>) -> String?,
modifier: Modifier = Modifier,
supportingText: String? = null,
@ -87,9 +87,9 @@ fun MultipleSelectionListItem(
MultipleSelectionDialog(
title = headline,
options = options,
onConfirmClicked = { newSelectedIndexes ->
onConfirmClick = { newSelectedIndexes ->
if (newSelectedIndexes != selectedIndexes.toList()) {
onSelectionChanged(newSelectedIndexes)
onSelectionChange(newSelectedIndexes)
selectedIndexes.clear()
selectedIndexes.addAll(newSelectedIndexes)
}
@ -109,7 +109,7 @@ internal fun MutipleSelectionListItemPreview() {
MultipleSelectionListItem(
headline = "Headline",
options = options,
onSelectionChanged = {},
onSelectionChange = {},
supportingText = "Supporting text",
resultFormatter = { result -> formatResult(result, options) },
)
@ -125,7 +125,7 @@ internal fun MutipleSelectionListItemSelectedPreview() {
MultipleSelectionListItem(
headline = "Headline",
options = options,
onSelectionChanged = {},
onSelectionChange = {},
supportingText = "Supporting text",
resultFormatter = {
val selectedValues = formatResult(it, options)
@ -145,7 +145,7 @@ internal fun MutipleSelectionListItemSelectedTrailingContentPreview() {
MultipleSelectionListItem(
headline = "Headline",
options = options,
onSelectionChanged = {},
onSelectionChange = {},
supportingText = "Supporting text",
resultFormatter = { selected.size.toString() },
displayResultInTrailingContent = true,

View file

@ -26,7 +26,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
fun RadioButtonListItem(
headline: String,
selected: Boolean,
onSelected: () -> Unit,
onSelect: () -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
trailingContent: ListItemContent? = null,
@ -42,6 +42,6 @@ fun RadioButtonListItem(
trailingContent = trailingContent,
style = style,
enabled = enabled,
onClick = onSelected,
onClick = onSelect,
)
}

View file

@ -42,7 +42,7 @@ import kotlin.time.Duration.Companion.seconds
fun SingleSelectionListItem(
headline: String,
options: ImmutableList<ListOption>,
onSelectionChanged: (Int) -> Unit,
onSelectionChange: (Int) -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
leadingContent: ListItemContent? = null,
@ -86,9 +86,9 @@ fun SingleSelectionListItem(
SingleSelectionDialog(
title = headline,
options = options,
onOptionSelected = { index ->
onSelectOption = { index ->
if (index != selectedIndex) {
onSelectionChanged(index)
onSelectionChange(index)
selectedIndex = index
}
// Delay hiding the dialog for a bit so the new state is displayed in it before being dismissed
@ -110,7 +110,7 @@ internal fun SingleSelectionListItemPreview() {
SingleSelectionListItem(
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
onSelectionChanged = {},
onSelectionChange = {},
)
}
}
@ -123,7 +123,7 @@ internal fun SingleSelectionListItemUnselectedWithSupportingTextPreview() {
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
supportingText = "Supporting text",
onSelectionChanged = {},
onSelectionChange = {},
)
}
}
@ -136,7 +136,7 @@ internal fun SingleSelectionListItemSelectedInSupportingTextPreview() {
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
supportingText = "Supporting text",
onSelectionChanged = {},
onSelectionChange = {},
selected = 1,
)
}
@ -150,7 +150,7 @@ internal fun SingleSelectionListItemSelectedInTrailingContentPreview() {
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
supportingText = "Supporting text",
onSelectionChanged = {},
onSelectionChange = {},
selected = 1,
displayResultInTrailingContent = true,
)
@ -165,7 +165,7 @@ internal fun SingleSelectionListItemCustomFormattertPreview() {
headline = "Headline",
options = listOptionOf("Option 1", "Option 2", "Option 3"),
supportingText = "Supporting text",
onSelectionChanged = {},
onSelectionChange = {},
resultFormatter = { "Selected index: $it" },
selected = 1,
displayResultInTrailingContent = true,

View file

@ -34,7 +34,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
fun TextFieldListItem(
placeholder: String?,
text: String,
onTextChanged: (String) -> Unit,
onTextChange: (String) -> Unit,
modifier: Modifier = Modifier,
error: String? = null,
maxLines: Int = 1,
@ -45,7 +45,7 @@ fun TextFieldListItem(
OutlinedTextField(
value = text,
onValueChange = { onTextChanged(it) },
onValueChange = { onTextChange(it) },
placeholder = placeholder?.let { @Composable { Text(it) } },
colors = OutlinedTextFieldDefaults.colors(
disabledBorderColor = Color.Transparent,
@ -68,7 +68,7 @@ fun TextFieldListItem(
fun TextFieldListItem(
placeholder: String?,
text: TextFieldValue,
onTextChanged: (TextFieldValue) -> Unit,
onTextChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
error: String? = null,
maxLines: Int = 1,
@ -79,7 +79,7 @@ fun TextFieldListItem(
OutlinedTextField(
value = text,
onValueChange = { onTextChanged(it) },
onValueChange = { onTextChange(it) },
placeholder = placeholder?.let { @Composable { Text(it) } },
colors = OutlinedTextFieldDefaults.colors(
disabledBorderColor = Color.Transparent,
@ -105,7 +105,7 @@ internal fun TextFieldListItemEmptyPreview() {
TextFieldListItem(
placeholder = "Placeholder",
text = "",
onTextChanged = {},
onTextChange = {},
)
}
}
@ -117,7 +117,7 @@ internal fun TextFieldListItemPreview() {
TextFieldListItem(
placeholder = "Placeholder",
text = "Text",
onTextChanged = {},
onTextChange = {},
)
}
}
@ -129,7 +129,7 @@ internal fun TextFieldListItemTextFieldValuePreview() {
TextFieldListItem(
placeholder = "Placeholder",
text = TextFieldValue("Text field value"),
onTextChanged = {},
onTextChange = {},
)
}
}

View file

@ -19,22 +19,19 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
@Composable
fun PreferenceCategory(
modifier: Modifier = Modifier,
title: String? = null,
showDivider: Boolean = true,
showTopDivider: Boolean = true,
content: @Composable ColumnScope.() -> Unit,
) {
Column(
@ -42,30 +39,17 @@ fun PreferenceCategory(
.fillMaxWidth()
) {
if (title != null) {
PreferenceCategoryTitle(title = title)
}
content()
if (showDivider) {
ListSectionHeader(
title = title,
hasDivider = showTopDivider,
)
} else if (showTopDivider) {
PreferenceDivider()
}
content()
}
}
@Composable
private fun PreferenceCategoryTitle(title: String) {
Text(
modifier = Modifier.padding(
top = 20.dp,
bottom = 8.dp,
start = preferencePaddingHorizontal,
end = preferencePaddingHorizontal,
),
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.materialColors.primary,
text = title,
)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceCategoryPreview() = ElementThemedPreview {

View file

@ -17,25 +17,18 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Checkbox
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
@ -52,45 +45,36 @@ fun PreferenceCheckbox(
@DrawableRes iconResourceId: Int? = null,
showIconAreaIfNoIcon: Boolean = false,
) {
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.clickable { onCheckedChange(!isChecked) }
.padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
verticalAlignment = Alignment.CenterVertically
) {
PreferenceIcon(
ListItem(
modifier = modifier,
onClick = onCheckedChange.takeIf { enabled }?.let { { onCheckedChange(!isChecked) } },
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
enabled = enabled,
isVisible = showIconAreaIfNoIcon
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
),
headlineContent = {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
)
if (supportingText != null) {
},
supportingContent = supportingText?.let {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = supportingText,
text = it,
color = enabled.toSecondaryEnabledColor(),
)
}
}
Checkbox(
modifier = Modifier
.align(Alignment.CenterVertically),
},
trailingContent = ListItemContent.Checkbox(
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange
)
}
),
)
}
@Preview(group = PreviewGroup.Preferences)
@ -112,5 +96,31 @@ internal fun PreferenceCheckboxPreview() = ElementThemedPreview {
isChecked = true,
onCheckedChange = {},
)
PreferenceCheckbox(
title = "Checkbox with supporting text",
supportingText = "Supporting text",
iconResourceId = CompoundDrawables.ic_compound_threads,
enabled = false,
isChecked = true,
onCheckedChange = {},
)
PreferenceCheckbox(
title = "Checkbox with supporting text",
supportingText = "Supporting text",
iconResourceId = null,
showIconAreaIfNoIcon = true,
enabled = true,
isChecked = true,
onCheckedChange = {},
)
PreferenceCheckbox(
title = "Checkbox with supporting text",
supportingText = "Supporting text",
iconResourceId = null,
showIconAreaIfNoIcon = false,
enabled = true,
isChecked = true,
onCheckedChange = {},
)
}
}

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
@ -28,10 +27,7 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi
fun PreferenceDivider(
modifier: Modifier = Modifier,
) {
HorizontalDivider(
modifier = modifier,
color = ElementTheme.colors.borderDisabled,
)
HorizontalDivider(modifier = modifier)
}
@Preview(group = PreviewGroup.Preferences)

View file

@ -44,7 +44,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
@Composable
fun PreferencePage(
title: String,
onBackPressed: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
snackbarHost: @Composable () -> Unit = {},
content: @Composable ColumnScope.() -> Unit,
@ -58,7 +58,7 @@ fun PreferencePage(
topBar = {
PreferenceTopAppBar(
title = title,
onBackPressed = onBackPressed,
onBackClick = onBackClick,
)
},
snackbarHost = snackbarHost,
@ -79,11 +79,11 @@ fun PreferencePage(
@Composable
private fun PreferenceTopAppBar(
title: String,
onBackPressed: () -> Unit,
onBackClick: () -> Unit,
) {
TopAppBar(
navigationIcon = {
BackButton(onClick = onBackPressed)
BackButton(onClick = onBackClick)
},
title = {
Text(
@ -101,7 +101,7 @@ private fun PreferenceTopAppBar(
internal fun PreferencePagePreview() = ElementPreview {
PreferencePage(
title = "Preference screen",
onBackPressed = {},
onBackClick = {},
) {
PreferenceCategory(
title = "Category title",

View file

@ -19,14 +19,13 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
/**
@ -37,15 +36,17 @@ fun PreferenceRow(
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit,
) {
Row(
modifier = modifier
.padding(horizontal = preferencePaddingHorizontal)
.heightIn(min = preferenceMinHeight)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
content()
}
ListItem(
modifier = modifier,
headlineContent = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
content()
}
}
)
}
@Preview(group = PreviewGroup.Preferences)

View file

@ -19,23 +19,18 @@ package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Slider
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
@Composable
fun PreferenceSlide(
@ -51,51 +46,57 @@ fun PreferenceSlide(
summary: String? = null,
steps: Int = 0,
) {
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
) {
PreferenceIcon(
ListItem(
modifier = modifier,
enabled = enabled,
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
isVisible = showIconAreaIfNoIcon,
)
Column(
modifier = Modifier
.weight(1f),
) {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
)
summary?.let {
enabled = enabled,
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
),
headlineContent = {
Column {
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = summary,
color = enabled.toEnabledColor(),
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
)
summary?.let {
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = summary,
)
}
Slider(
value = value,
steps = steps,
onValueChange = onValueChange,
enabled = enabled,
)
}
Slider(
value = value,
steps = steps,
onValueChange = onValueChange,
enabled = enabled,
)
}
}
)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceSlidePreview() = ElementThemedPreview {
PreferenceSlide(
icon = CompoundIcons.UserProfile(),
title = "Slide",
summary = "Summary",
value = 0.75F,
onValueChange = {},
)
Column {
PreferenceSlide(
icon = CompoundIcons.UserProfile(),
title = "Slide",
summary = "Summary",
enabled = true,
value = 0.75F,
onValueChange = {},
)
PreferenceSlide(
icon = CompoundIcons.UserProfile(),
title = "Slide",
summary = "Summary",
enabled = false,
value = 0.75F,
onValueChange = {},
)
}
}

View file

@ -17,30 +17,19 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Switch
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
@Composable
fun PreferenceSwitch(
@ -53,62 +42,65 @@ fun PreferenceSwitch(
icon: ImageVector? = null,
@DrawableRes iconResourceId: Int? = null,
showIconAreaIfNoIcon: Boolean = false,
switchAlignment: Alignment.Vertical = Alignment.CenterVertically
) {
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = preferenceMinHeight)
.clickable { onCheckedChange(!isChecked) }
.padding(vertical = 4.dp, horizontal = preferencePaddingHorizontal),
verticalAlignment = Alignment.CenterVertically
) {
PreferenceIcon(
ListItem(
modifier = modifier,
enabled = enabled,
onClick = onCheckedChange.takeIf { enabled }?.let { { onCheckedChange(!isChecked) } },
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
enabled = enabled,
isVisible = showIconAreaIfNoIcon
)
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
),
headlineContent = {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = enabled.toEnabledColor(),
)
if (subtitle != null) {
Spacer(modifier = Modifier.height(4.dp))
},
supportingContent = subtitle?.let {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
color = enabled.toSecondaryEnabledColor(),
)
}
}
Spacer(modifier = Modifier.width(16.dp))
// TODO Create a wrapper for Switch
Switch(
modifier = Modifier
.align(switchAlignment),
},
trailingContent = ListItemContent.Switch(
checked = isChecked,
enabled = enabled,
onCheckedChange = onCheckedChange
)
}
)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceSwitchPreview() = ElementThemedPreview {
PreferenceSwitch(
title = "Switch",
subtitle = "Subtitle Switch",
icon = CompoundIcons.Threads(),
enabled = true,
isChecked = true,
onCheckedChange = {},
)
Column {
PreferenceSwitch(
title = "Switch",
subtitle = "Subtitle Switch",
icon = CompoundIcons.Threads(),
enabled = true,
isChecked = true,
onCheckedChange = {},
)
PreferenceSwitch(
title = "Switch",
subtitle = "Subtitle Switch",
icon = CompoundIcons.Threads(),
enabled = false,
isChecked = true,
onCheckedChange = {},
)
PreferenceSwitch(
title = "Switch no subtitle",
subtitle = null,
icon = CompoundIcons.Threads(),
enabled = false,
isChecked = true,
onCheckedChange = {},
)
}
}

View file

@ -17,12 +17,9 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
@ -38,18 +35,17 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
/**
* Tried to use ListItem, but it cannot really match the design. Keep custom Layout for now.
*/
@Composable
fun PreferenceText(
title: String,
@ -67,76 +63,76 @@ fun PreferenceText(
tintColor: Color? = null,
onClick: () -> Unit = {},
) {
val minHeight = if (subtitle == null && subtitleAnnotated == null) preferenceMinHeightOnlyTitle else preferenceMinHeight
Row(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = minHeight)
.clickable { onClick() }
.padding(horizontal = preferencePaddingHorizontal, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
PreferenceIcon(
ListItem(
modifier = modifier,
enabled = enabled,
onClick = onClick,
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
showIconBadge = showIconBadge,
enabled = enabled,
isVisible = showIconAreaIfNoIcon,
tintColor = tintColor ?: enabled.toSecondaryEnabledColor(),
)
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
) {
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
tintColor = tintColor,
),
headlineContent = {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = tintColor ?: enabled.toEnabledColor(),
)
if (subtitle != null) {
},
supportingContent = if (subtitle != null) {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
color = tintColor ?: enabled.toSecondaryEnabledColor(),
)
} else if (subtitleAnnotated != null) {
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitleAnnotated,
color = tintColor ?: enabled.toSecondaryEnabledColor(),
)
}
} else {
subtitleAnnotated?.let {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = it,
color = tintColor ?: enabled.toSecondaryEnabledColor(),
)
}
}
},
trailingContent = if (currentValue != null || loadingCurrentValue || showEndBadge) {
ListItemContent.Custom {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
if (currentValue != null) {
Text(
text = currentValue,
style = ElementTheme.typography.fontBodyXsMedium,
color = enabled.toSecondaryEnabledColor(),
)
} else if (loadingCurrentValue) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(20.dp),
strokeWidth = 2.dp
)
}
if (showEndBadge) {
val endBadgeStartPadding = if (currentValue != null || loadingCurrentValue) 16.dp else 0.dp
RedIndicatorAtom(
modifier = Modifier
.padding(start = endBadgeStartPadding)
)
}
}
}
} else {
null
}
if (currentValue != null) {
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 16.dp, end = 8.dp),
text = currentValue,
style = ElementTheme.typography.fontBodyXsMedium,
color = enabled.toSecondaryEnabledColor(),
)
} else if (loadingCurrentValue) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.padding(start = 16.dp, end = 8.dp)
.size(20.dp)
.align(Alignment.CenterVertically),
strokeWidth = 2.dp
)
}
if (showEndBadge) {
val endBadgeStartPadding = if (currentValue != null || loadingCurrentValue) 8.dp else 16.dp
RedIndicatorAtom(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = endBadgeStartPadding)
)
}
}
)
}
@Preview(group = PreviewGroup.Preferences)

View file

@ -114,7 +114,7 @@ private fun TextFieldDialog(
TextFieldListItem(
placeholder = placeholder.orEmpty(),
text = textFieldContents,
onTextChanged = {
onTextChange = {
error = if (!validation(it.text)) onValidationErrorMessage else null
textFieldContents = it
},

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.designsystem.components.preferences.compone
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
@ -31,13 +30,39 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
@Composable
fun PreferenceIcon(
fun preferenceIcon(
icon: ImageVector? = null,
@DrawableRes iconResourceId: Int? = null,
showIconBadge: Boolean = false,
tintColor: Color? = null,
enabled: Boolean = true,
showIconAreaIfNoIcon: Boolean = false,
): ListItemContent.Custom? {
return if (icon != null || iconResourceId != null || showIconAreaIfNoIcon) {
ListItemContent.Custom {
PreferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
showIconBadge = showIconBadge,
enabled = enabled,
isVisible = showIconAreaIfNoIcon,
tintColor = tintColor,
)
}
} else {
null
}
}
@Composable
private fun PreferenceIcon(
modifier: Modifier = Modifier,
icon: ImageVector? = null,
@DrawableRes iconResourceId: Int? = null,
@ -54,19 +79,17 @@ fun PreferenceIcon(
contentDescription = null,
tint = tintColor ?: enabled.toSecondaryEnabledColor(),
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp),
)
if (showIconBadge) {
RedIndicatorAtom(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(end = 16.dp)
)
}
}
} else if (isVisible) {
Spacer(modifier = modifier.width(40.dp))
Spacer(modifier = modifier.width(24.dp))
}
}

View file

@ -16,30 +16,26 @@
package io.element.android.libraries.designsystem.modifiers
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.inspectable
/**
* Applies the [ifTrue] modifier when the [condition] is true, [ifFalse] otherwise.
*/
@SuppressLint("UnnecessaryComposedModifier") // It's actually necessary due to the `@Composable` lambdas
fun Modifier.applyIf(
condition: Boolean,
ifTrue: @Composable Modifier.() -> Modifier,
ifFalse: @Composable (Modifier.() -> Modifier)? = null
): Modifier =
composed(
inspectorInfo = debugInspectorInfo {
name = "applyIf"
value = condition
}
) {
when {
condition -> then(ifTrue(Modifier))
ifFalse != null -> then(ifFalse(Modifier))
else -> this
}
ifTrue: Modifier.() -> Modifier,
ifFalse: (Modifier.() -> Modifier)? = null
): Modifier = this then inspectable(
inspectorInfo = debugInspectorInfo {
name = "applyIf"
value = condition
}
) {
this then when {
condition -> ifTrue(Modifier)
ifFalse != null -> ifFalse(Modifier)
else -> Modifier
}
}

View file

@ -54,14 +54,14 @@ import kotlin.math.max
internal fun SimpleAlertDialogContent(
content: String,
submitText: String,
onSubmitClicked: () -> Unit,
onSubmitClick: () -> Unit,
title: String? = null,
subtitle: @Composable (() -> Unit)? = null,
destructiveSubmit: Boolean = false,
cancelText: String? = null,
onCancelClicked: () -> Unit = {},
onCancelClick: () -> Unit = {},
thirdButtonText: String? = null,
onThirdButtonClicked: () -> Unit = {},
onThirdButtonClick: () -> Unit = {},
applyPaddingToContents: Boolean = true,
icon: @Composable (() -> Unit)? = null,
) {
@ -77,11 +77,11 @@ internal fun SimpleAlertDialogContent(
},
submitText = submitText,
destructiveSubmit = destructiveSubmit,
onSubmitClicked = onSubmitClicked,
onSubmitClick = onSubmitClick,
cancelText = cancelText,
onCancelClicked = onCancelClicked,
onCancelClick = onCancelClick,
thirdButtonText = thirdButtonText,
onThirdButtonClicked = onThirdButtonClicked,
onThirdButtonClick = onThirdButtonClick,
applyPaddingToContents = applyPaddingToContents,
)
}
@ -89,14 +89,14 @@ internal fun SimpleAlertDialogContent(
@Composable
internal fun SimpleAlertDialogContent(
submitText: String,
onSubmitClicked: () -> Unit,
onSubmitClick: () -> Unit,
title: String? = null,
subtitle: @Composable (() -> Unit)? = null,
destructiveSubmit: Boolean = false,
cancelText: String? = null,
onCancelClicked: () -> Unit = {},
onCancelClick: () -> Unit = {},
thirdButtonText: String? = null,
onThirdButtonClicked: () -> Unit = {},
onThirdButtonClick: () -> Unit = {},
applyPaddingToContents: Boolean = true,
enabled: Boolean = true,
icon: @Composable (() -> Unit)? = null,
@ -115,7 +115,7 @@ internal fun SimpleAlertDialogContent(
modifier = Modifier.testTag(TestTags.dialogNeutral),
text = thirdButtonText,
size = ButtonSize.Medium,
onClick = onThirdButtonClicked,
onClick = onThirdButtonClick,
)
}
if (cancelText != null) {
@ -123,14 +123,14 @@ internal fun SimpleAlertDialogContent(
modifier = Modifier.testTag(TestTags.dialogNegative),
text = cancelText,
size = ButtonSize.Medium,
onClick = onCancelClicked,
onClick = onCancelClick,
)
Button(
modifier = Modifier.testTag(TestTags.dialogPositive),
text = submitText,
enabled = enabled,
size = ButtonSize.Medium,
onClick = onSubmitClicked,
onClick = onSubmitClick,
destructive = destructiveSubmit,
)
} else {
@ -139,7 +139,7 @@ internal fun SimpleAlertDialogContent(
text = submitText,
enabled = enabled,
size = ButtonSize.Medium,
onClick = onSubmitClicked,
onClick = onSubmitClick,
destructive = destructiveSubmit,
)
}
@ -174,6 +174,7 @@ internal fun SimpleAlertDialogContent(
/**
* Copy of M3's `AlertDialogContent` so we can use it for previews.
*/
@Suppress("ContentTrailingLambda")
@Composable
internal fun AlertDialogContent(
buttons: @Composable () -> Unit,
@ -444,7 +445,7 @@ internal fun DialogWithTitleIconAndOkButtonPreview() {
content = "A dialog is a type of modal window that appears in front of app content to provide critical information," +
" or prompt for a decision to be made. Learn more",
submitText = "OK",
onSubmitClicked = {},
onSubmitClick = {},
)
}
}
@ -461,7 +462,7 @@ internal fun DialogWithTitleAndOkButtonPreview() {
content = "A dialog is a type of modal window that appears in front of app content to provide critical information," +
" or prompt for a decision to be made. Learn more",
submitText = "OK",
onSubmitClicked = {},
onSubmitClick = {},
)
}
}
@ -477,7 +478,7 @@ internal fun DialogWithOnlyMessageAndOkButtonPreview() {
content = "A dialog is a type of modal window that appears in front of app content to provide critical information," +
" or prompt for a decision to be made. Learn more",
submitText = "OK",
onSubmitClicked = {},
onSubmitClick = {},
)
}
}
@ -494,7 +495,7 @@ internal fun DialogWithDestructiveButtonPreview() {
cancelText = "Cancel",
submitText = "Delete",
destructiveSubmit = true,
onSubmitClicked = {},
onSubmitClick = {},
)
}
}
@ -511,7 +512,7 @@ internal fun DialogWithThirdButtonPreview() {
cancelText = "Cancel",
submitText = "Delete",
thirdButtonText = "Other",
onSubmitClicked = {},
onSubmitClick = {},
)
}
}

View file

@ -1,132 +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.
*/
// This is actually expected, as we should remove this component soon and use ModalBottomSheet instead
@file:Suppress("UsingMaterialAndMaterial3Libraries")
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetDefaults
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.modifiers.applyIf
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewGroup
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ModalBottomSheetLayout(
sheetContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
sheetShape: Shape = MaterialTheme.shapes.large.copy(bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)),
sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface,
sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
displayHandle: Boolean = false,
useSystemPadding: Boolean = true,
content: @Composable () -> Unit = {}
) {
androidx.compose.material.ModalBottomSheetLayout(
sheetContent = {
Column(
Modifier.fillMaxWidth()
.applyIf(useSystemPadding, ifTrue = {
navigationBarsPadding()
})
) {
if (displayHandle) {
Spacer(modifier = Modifier.height(16.dp))
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.onSurfaceVariant, RoundedCornerShape(2.dp))
.size(width = 32.dp, height = 4.dp)
.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(24.dp))
}
sheetContent()
}
},
modifier = modifier,
sheetState = sheetState,
sheetShape = sheetShape,
sheetElevation = sheetElevation,
sheetBackgroundColor = sheetBackgroundColor,
sheetContentColor = sheetContentColor,
scrimColor = scrimColor,
content = content,
)
}
@Preview(group = PreviewGroup.BottomSheets)
@Composable
internal fun ModalBottomSheetLayoutLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview(group = PreviewGroup.BottomSheets)
@Composable
internal fun ModalBottomSheetLayoutDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@OptIn(ExperimentalMaterialApi::class)
@ExcludeFromCoverage
@Composable
private fun ContentToPreview() {
ModalBottomSheetLayout(
modifier = Modifier.height(140.dp),
displayHandle = true,
sheetState = ModalBottomSheetState(ModalBottomSheetValue.Expanded, density = LocalDensity.current),
sheetContent = {
Text(
text = "Sheet Content",
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 20.dp)
.background(color = Color.Green)
)
}
) {
Text(text = "Content", modifier = Modifier.background(color = Color.Red))
}
}

View file

@ -39,7 +39,7 @@ fun Slider(
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
// @IntRange(from = 0)
steps: Int = 0,
onValueChangeFinished: (() -> Unit)? = null,
onValueChangeFinish: (() -> Unit)? = null,
colors: SliderColors = SliderDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
@ -50,7 +50,7 @@ fun Slider(
enabled = enabled,
valueRange = valueRange,
steps = steps,
onValueChangeFinished = onValueChangeFinished,
onValueChangeFinished = onValueChangeFinish,
colors = colors,
interactionSource = interactionSource,
)

View file

@ -213,6 +213,7 @@ private fun TextFieldValueContentToPreview() {
}
}
@Suppress("ModifierComposed")
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.autofill(autofillTypes: List<AutofillType>, onFill: (String) -> Unit) = composed {
val autofillNode = AutofillNode(autofillTypes, onFill = onFill)

View file

@ -34,6 +34,7 @@ class RoomMembershipContentFormatter @Inject constructor(
): CharSequence? {
val userId = membershipContent.userId
val memberIsYou = matrixClient.isMe(userId)
val userDisplayNameOrId = membershipContent.userDisplayName ?: userId.value
return when (membershipContent.change) {
MembershipChange.JOINED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_join_by_you)
@ -46,41 +47,41 @@ class RoomMembershipContentFormatter @Inject constructor(
sp.getString(R.string.state_event_room_leave, senderDisambiguatedDisplayName)
}
MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_ban_by_you, userId.value)
sp.getString(R.string.state_event_room_ban_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_ban, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_ban, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.UNBANNED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_unban_by_you, userId.value)
sp.getString(R.string.state_event_room_unban_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_unban, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_unban, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.KICKED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_remove_by_you, userId.value)
sp.getString(R.string.state_event_room_remove_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_remove, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_remove, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.INVITED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_invite_by_you, userId.value)
sp.getString(R.string.state_event_room_invite_by_you, userDisplayNameOrId)
} else if (memberIsYou) {
sp.getString(R.string.state_event_room_invite_you, senderDisambiguatedDisplayName)
} else {
sp.getString(R.string.state_event_room_invite, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_invite, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_invite_accepted_by_you)
} else {
sp.getString(R.string.state_event_room_invite_accepted, userId.value)
sp.getString(R.string.state_event_room_invite_accepted, userDisplayNameOrId)
}
MembershipChange.INVITATION_REJECTED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_reject_by_you)
} else {
sp.getString(R.string.state_event_room_reject, userId.value)
sp.getString(R.string.state_event_room_reject, userDisplayNameOrId)
}
MembershipChange.INVITATION_REVOKED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value)
sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.KNOCKED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_knock_by_you)
@ -88,9 +89,9 @@ class RoomMembershipContentFormatter @Inject constructor(
sp.getString(R.string.state_event_room_knock, senderDisambiguatedDisplayName)
}
MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value)
sp.getString(R.string.state_event_room_knock_accepted_by_you, userDisplayNameOrId)
} else {
sp.getString(R.string.state_event_room_knock_accepted, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_knock_accepted, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_knock_retracted_by_you)
@ -98,11 +99,11 @@ class RoomMembershipContentFormatter @Inject constructor(
sp.getString(R.string.state_event_room_knock_retracted, senderDisambiguatedDisplayName)
}
MembershipChange.KNOCK_DENIED -> if (senderIsYou) {
sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value)
sp.getString(R.string.state_event_room_knock_denied_by_you, userDisplayNameOrId)
} else if (memberIsYou) {
sp.getString(R.string.state_event_room_knock_denied_you, senderDisambiguatedDisplayName)
} else {
sp.getString(R.string.state_event_room_knock_denied, senderDisambiguatedDisplayName, userId.value)
sp.getString(R.string.state_event_room_knock_denied, senderDisambiguatedDisplayName, userDisplayNameOrId)
}
MembershipChange.NONE -> if (senderIsYou) {
sp.getString(R.string.state_event_room_none_by_you)

View file

@ -3,12 +3,16 @@
<string name="state_event_avatar_changed_too">"(el avatar también cambió)"</string>
<string name="state_event_avatar_url_changed">"%1$s cambió su avatar"</string>
<string name="state_event_avatar_url_changed_by_you">"Cambiaste tu avatar"</string>
<string name="state_event_demoted_to_member">"%1$s fue degradado a miembro"</string>
<string name="state_event_demoted_to_moderator">"%1$s fue degradado a moderador"</string>
<string name="state_event_display_name_changed_from">"%1$s cambió su nombre de %2$s a %3$s"</string>
<string name="state_event_display_name_changed_from_by_you">"Cambiaste tu nombre de %1$s a %2$s"</string>
<string name="state_event_display_name_removed">"%1$s eliminó su nombre (era %2$s)"</string>
<string name="state_event_display_name_removed_by_you">"Eliminaste tu nombre (era %1$s)"</string>
<string name="state_event_display_name_set">"%1$s cambió su nombre a %2$s"</string>
<string name="state_event_display_name_set_by_you">"Cambiaste tu nombre a %1$s"</string>
<string name="state_event_promoted_to_administrator">"%1$s fue ascendido a administrador"</string>
<string name="state_event_promoted_to_moderator">"%1$s fue ascendido a moderador"</string>
<string name="state_event_room_avatar_changed">"%1$s cambió el avatar de la sala"</string>
<string name="state_event_room_avatar_changed_by_you">"Cambiaste el avatar de la sala"</string>
<string name="state_event_room_avatar_removed">"%1$s eliminó el avatar de la sala"</string>
@ -39,6 +43,8 @@
<string name="state_event_room_name_changed_by_you">"Cambiaste el nombre de la sala a: %1$s"</string>
<string name="state_event_room_name_removed">"%1$s eliminó el nombre de la sala"</string>
<string name="state_event_room_name_removed_by_you">"Eliminaste el nombre de la sala"</string>
<string name="state_event_room_none">"%1$s no hizo cambios"</string>
<string name="state_event_room_none_by_you">"No has hecho ningún cambio"</string>
<string name="state_event_room_reject">"%1$s rechazó la invitación"</string>
<string name="state_event_room_reject_by_you">"Rechazaste la invitación"</string>
<string name="state_event_room_remove">"%1$s echó a %2$s"</string>

View file

@ -30,7 +30,7 @@
<string name="state_event_room_join_by_you">"Ti sei unito alla stanza"</string>
<string name="state_event_room_knock">"%1$s ha chiesto di unirsi"</string>
<string name="state_event_room_knock_accepted">"%1$s ha permesso a %2$s di unirsi"</string>
<string name="state_event_room_knock_accepted_by_you">"%1$s ti ha permesso di unirti"</string>
<string name="state_event_room_knock_accepted_by_you">"Hai permesso a %1$s di partecipare"</string>
<string name="state_event_room_knock_by_you">"Hai richiesto di unirti"</string>
<string name="state_event_room_knock_denied">"%1$s ha rifiutato la richiesta di unirsi di %2$s"</string>
<string name="state_event_room_knock_denied_by_you">"Hai rifiutato la richiesta di unirsi di %1$s"</string>

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

@ -0,0 +1,63 @@
<?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">"(avatar alterado também)"</string>
<string name="state_event_avatar_url_changed">"%1$s alterou o seu avatar"</string>
<string name="state_event_avatar_url_changed_by_you">"Alteraste o teu avatar"</string>
<string name="state_event_demoted_to_member">"%1$s foi despromovido a participante"</string>
<string name="state_event_demoted_to_moderator">"%1$s foi despromovido a moderador"</string>
<string name="state_event_display_name_changed_from">"%1$s alterou o seu pseudónimo de %2$s para %3$s"</string>
<string name="state_event_display_name_changed_from_by_you">"Alteraste o teu pseudónimo de %1$s para %2$s"</string>
<string name="state_event_display_name_removed">"%1$s removeu o seu pseudónimo (era %2$s)"</string>
<string name="state_event_display_name_removed_by_you">"Removeste o teu pseudónimo (era %1$s)"</string>
<string name="state_event_display_name_set">"%1$s definiu o seu pseudónimo como %2$s"</string>
<string name="state_event_display_name_set_by_you">"Definiste o teu pseudónimo como %1$s"</string>
<string name="state_event_promoted_to_administrator">"%1$s foi promovido a administrador"</string>
<string name="state_event_promoted_to_moderator">"%1$s foi promovido a moderador"</string>
<string name="state_event_room_avatar_changed">"%1$s alterou o ícone da sala"</string>
<string name="state_event_room_avatar_changed_by_you">"Alteraste o ícone da sala"</string>
<string name="state_event_room_avatar_removed">"%1$s removeu o ícone da sala"</string>
<string name="state_event_room_avatar_removed_by_you">"Removeste o ícone da sala"</string>
<string name="state_event_room_ban">"%1$s baniu %2$s"</string>
<string name="state_event_room_ban_by_you">"Baniste %1$s"</string>
<string name="state_event_room_created">"%1$s criou a sala"</string>
<string name="state_event_room_created_by_you">"Criaste a sala"</string>
<string name="state_event_room_invite">"%1$s convidou %2$s"</string>
<string name="state_event_room_invite_accepted">"%1$s aceitou o convite"</string>
<string name="state_event_room_invite_accepted_by_you">"Aceitaste o convite"</string>
<string name="state_event_room_invite_by_you">"Convidaste %1$s"</string>
<string name="state_event_room_invite_you">"%1$s convidou-te"</string>
<string name="state_event_room_join">"%1$s entrou na sala"</string>
<string name="state_event_room_join_by_you">"Entraste na sala"</string>
<string name="state_event_room_knock">"%1$s pediu para entrar"</string>
<string name="state_event_room_knock_accepted">"%1$s permitiu %2$s entrar"</string>
<string name="state_event_room_knock_accepted_by_you">"Permitiste a entrada de %1$s"</string>
<string name="state_event_room_knock_by_you">"Pediste para entrar"</string>
<string name="state_event_room_knock_denied">"%1$s rejeitou o pedido de entrada de %2$s"</string>
<string name="state_event_room_knock_denied_by_you">"Rejeitaste o pedido de entrada e %1$s"</string>
<string name="state_event_room_knock_denied_you">"%1$s rejeitou o teu pedido de entrada"</string>
<string name="state_event_room_knock_retracted">"%1$s deixou de querer entrar"</string>
<string name="state_event_room_knock_retracted_by_you">"Cancelaste o teu pedido de entrada"</string>
<string name="state_event_room_leave">"%1$s saiu da sala"</string>
<string name="state_event_room_leave_by_you">"Saíste da sala"</string>
<string name="state_event_room_name_changed">"%1$s alterou o nome da sala para: %2$s"</string>
<string name="state_event_room_name_changed_by_you">"Alteraste o nome da sala para:%1$s"</string>
<string name="state_event_room_name_removed">"%1$s removeu o nome da sala"</string>
<string name="state_event_room_name_removed_by_you">"Removeste o nome da sala"</string>
<string name="state_event_room_none">"%1$s não fiz nenhuma alteração"</string>
<string name="state_event_room_none_by_you">"Não fizeste nenhuma alteração"</string>
<string name="state_event_room_reject">"%1$s rejeitou o convite"</string>
<string name="state_event_room_reject_by_you">"Rejeitaste o convite"</string>
<string name="state_event_room_remove">"%1$s removeu %2$s"</string>
<string name="state_event_room_remove_by_you">"Removeste %1$s"</string>
<string name="state_event_room_third_party_invite">"%1$s enviou um convite a %2$s"</string>
<string name="state_event_room_third_party_invite_by_you">"Enviaste um convite a %1$s"</string>
<string name="state_event_room_third_party_revoked_invite">"%1$s revogou o convite de %2$s"</string>
<string name="state_event_room_third_party_revoked_invite_by_you">"Revogaste o convite de %1$s"</string>
<string name="state_event_room_topic_changed">"%1$s alterou a descrição para: %2$s"</string>
<string name="state_event_room_topic_changed_by_you">"Alteraste a descrição para: %1$s"</string>
<string name="state_event_room_topic_removed">"%1$s removeu a descrição da sala"</string>
<string name="state_event_room_topic_removed_by_you">"Removeste a descrição da sala"</string>
<string name="state_event_room_unban">"%1$s desbaniu %2$s"</string>
<string name="state_event_room_unban_by_you">"Anulaste o banimento de %1$s"</string>
<string name="state_event_room_unknown_membership_change">"%1$s efetuou uma alteração desconhecida à sua participação na sala"</string>
</resources>

View file

@ -3,12 +3,16 @@
<string name="state_event_avatar_changed_too">"(s-a schimbat si avatarul)"</string>
<string name="state_event_avatar_url_changed">"%1$s și-a schimbat avatarul"</string>
<string name="state_event_avatar_url_changed_by_you">"V-ați schimbat avatarul"</string>
<string name="state_event_demoted_to_member">"%1$s a fost retrogradat la funcția de membru"</string>
<string name="state_event_demoted_to_moderator">"%1$s a fost retrogradat la funcția de moderator"</string>
<string name="state_event_display_name_changed_from">"%1$s și-a schimbat numele din %2$s în %3$s"</string>
<string name="state_event_display_name_changed_from_by_you">"V-ați schimbat numele din %1$s în %2$s"</string>
<string name="state_event_display_name_removed">"%1$s și-a sters numele (era %2$s)"</string>
<string name="state_event_display_name_removed_by_you">"V-ați sters numele (era %1$s)"</string>
<string name="state_event_display_name_set">"%1$s și-a schimbat numele %2$s"</string>
<string name="state_event_display_name_set_by_you">"V-ați schimbat numele în %1$s"</string>
<string name="state_event_promoted_to_administrator">"%1$s a fost promovat în funcția de administrator"</string>
<string name="state_event_promoted_to_moderator">"%1$s a fost promovat la funcția de moderator"</string>
<string name="state_event_room_avatar_changed">"%1$s a schimbat avatarul camerei"</string>
<string name="state_event_room_avatar_changed_by_you">"Ați schimbat avatarul camerei"</string>
<string name="state_event_room_avatar_removed">"%1$s a șters avatarul camerei"</string>
@ -26,7 +30,7 @@
<string name="state_event_room_join_by_you">"Ați intrat în cameră"</string>
<string name="state_event_room_knock">"%1$s a solicitat să se alăture camerei"</string>
<string name="state_event_room_knock_accepted">"%1$s i-a permis lui %2$s să se alăture camerei"</string>
<string name="state_event_room_knock_accepted_by_you">"%1$s v-a permis să vă alăturați camerei"</string>
<string name="state_event_room_knock_accepted_by_you">"I-ați permis lui %1$s să se alăture"</string>
<string name="state_event_room_knock_by_you">"Ați solicitat să vă alăturați camerei"</string>
<string name="state_event_room_knock_denied">"%1$s a respins solicitarea de alăturare a lui %2$s"</string>
<string name="state_event_room_knock_denied_by_you">"Ați respins solicitarea de alăturare a lui %1$s"</string>

View file

@ -30,7 +30,7 @@
<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_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>

View file

@ -0,0 +1,63 @@
<?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_demoted_to_member">"%1$s 降级为成员"</string>
<string name="state_event_demoted_to_moderator">"%1$s 降级为协管员"</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_promoted_to_administrator">"%1$s 晋升为管理员"</string>
<string name="state_event_promoted_to_moderator">"%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_none">"%1$s 没有任何更改"</string>
<string name="state_event_room_none_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

@ -254,9 +254,9 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - joined`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.JOINED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.JOINED)
val youJoinedRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youJoinedRoom = formatter.format(youJoinedRoomEvent, false)
@ -270,9 +270,9 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - left`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.LEFT)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.LEFT)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.LEFT)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.LEFT)
val youLeftRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youLeftRoom = formatter.format(youLeftRoomEvent, false)
@ -286,67 +286,71 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - banned`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.BANNED)
val youKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED_AND_BANNED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.BANNED)
val someoneKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED_AND_BANNED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED)
val youKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.BANNED)
val someoneKickedContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED_AND_BANNED)
val youBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youBanned = formatter.format(youBannedEvent, false)
assertThat(youBanned).isEqualTo("You banned ${youContent.userId}")
assertThat(youBanned).isEqualTo("You banned $third")
val youKickBannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youKickedContent)
val youKickedBanned = formatter.format(youKickBannedEvent, false)
assertThat(youKickedBanned).isEqualTo("You banned ${youContent.userId}")
assertThat(youKickedBanned).isEqualTo("You banned $third")
val someoneBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneBanned = formatter.format(someoneBannedEvent, false)
assertThat(someoneBanned).isEqualTo("$otherName banned ${someoneContent.userId}")
assertThat(someoneBanned).isEqualTo("$otherName banned $third")
val someoneKickBannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneKickedContent)
val someoneKickBanned = formatter.format(someoneKickBannedEvent, false)
assertThat(someoneKickBanned).isEqualTo("$otherName banned ${someoneContent.userId}")
assertThat(someoneKickBanned).isEqualTo("$otherName banned $third")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - unban`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.UNBANNED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.UNBANNED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.UNBANNED)
val youUnbannedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youUnbanned = formatter.format(youUnbannedEvent, false)
assertThat(youUnbanned).isEqualTo("You unbanned ${youContent.userId}")
assertThat(youUnbanned).isEqualTo("You unbanned $third")
val someoneUnbannedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneUnbanned = formatter.format(someoneUnbannedEvent, false)
assertThat(someoneUnbanned).isEqualTo("$otherName unbanned ${someoneContent.userId}")
assertThat(someoneUnbanned).isEqualTo("$otherName unbanned $third")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - kicked`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KICKED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KICKED)
val youKickedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youKicked = formatter.format(youKickedEvent, false)
assertThat(youKicked).isEqualTo("You removed ${youContent.userId}")
assertThat(youKicked).isEqualTo("You removed $third")
val someoneKickedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneKicked = formatter.format(someoneKickedEvent, false)
assertThat(someoneKicked).isEqualTo("$otherName removed ${someoneContent.userId}")
assertThat(someoneKicked).isEqualTo("$otherName removed $third")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - invited`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.INVITED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITED)
val youWereInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent)
val youWereInvited = formatter.format(youWereInvitedEvent, false)
@ -354,19 +358,19 @@ class DefaultRoomLastMessageFormatterTest {
val youInvitedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youInvited = formatter.format(youInvitedEvent, false)
assertThat(youInvited).isEqualTo("You invited ${someoneContent.userId}")
assertThat(youInvited).isEqualTo("You invited $third")
val someoneInvitedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneInvited = formatter.format(someoneInvitedEvent, false)
assertThat(someoneInvited).isEqualTo("$otherName invited ${someoneContent.userId}")
assertThat(someoneInvited).isEqualTo("$otherName invited $third")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - invitation accepted`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.INVITATION_ACCEPTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITATION_ACCEPTED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_ACCEPTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_ACCEPTED)
val youAcceptedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youAcceptedInvite = formatter.format(youAcceptedInviteEvent, false)
@ -374,15 +378,15 @@ class DefaultRoomLastMessageFormatterTest {
val someoneAcceptedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneAcceptedInvite = formatter.format(someoneAcceptedInviteEvent, false)
assertThat(someoneAcceptedInvite).isEqualTo("${someoneContent.userId} accepted the invite")
assertThat(someoneAcceptedInvite).isEqualTo("$otherName accepted the invite")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - invitation rejected`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.INVITATION_REJECTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITATION_REJECTED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.INVITATION_REJECTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.INVITATION_REJECTED)
val youRejectedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youRejectedInvite = formatter.format(youRejectedInviteEvent, false)
@ -390,30 +394,31 @@ class DefaultRoomLastMessageFormatterTest {
val someoneRejectedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneRejectedInvite = formatter.format(someoneRejectedInviteEvent, false)
assertThat(someoneRejectedInvite).isEqualTo("${someoneContent.userId} rejected the invitation")
assertThat(someoneRejectedInvite).isEqualTo("$otherName rejected the invitation")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - invitation revoked`() {
val otherName = "Someone"
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.INVITATION_REVOKED)
val otherName = "Other"
val third = "Someone"
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.INVITATION_REVOKED)
val youRevokedInviteEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youRevokedInvite = formatter.format(youRevokedInviteEvent, false)
assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for ${someoneContent.userId} to join the room")
assertThat(youRevokedInvite).isEqualTo("You revoked the invitation for $third to join the room")
val someoneRevokedInviteEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneRevokedInvite = formatter.format(someoneRevokedInviteEvent, false)
assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for ${someoneContent.userId} to join the room")
assertThat(someoneRevokedInvite).isEqualTo("$otherName revoked the invitation for $third to join the room")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - knocked`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.KNOCKED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCKED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCKED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.KNOCKED)
val youKnockedEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youKnocked = formatter.format(youKnockedEvent, false)
@ -427,24 +432,25 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - knock accepted`() {
val otherName = "Someone"
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCK_ACCEPTED)
val otherName = "Other"
val third = "Someone"
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_ACCEPTED)
val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youAcceptedKnock = formatter.format(youAcceptedKnockEvent, false)
assertThat(youAcceptedKnock).isEqualTo("You allowed ${someoneContent.userId} to join")
assertThat(youAcceptedKnock).isEqualTo("You allowed $third to join")
val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false)
assertThat(someoneAcceptedKnock).isEqualTo("$otherName allowed ${someoneContent.userId} to join")
assertThat(someoneAcceptedKnock).isEqualTo("$otherName allowed $third to join")
}
@Test
@Config(qualifiers = "en")
fun `Membership change - knock retracted`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.KNOCK_RETRACTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCK_RETRACTED)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.KNOCK_RETRACTED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), null, MembershipChange.KNOCK_RETRACTED)
val youRetractedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youRetractedKnock = formatter.format(youRetractedKnockEvent, false)
@ -458,17 +464,18 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - knock denied`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.KNOCK_DENIED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.KNOCK_DENIED)
val otherName = "Other"
val third = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, third, MembershipChange.KNOCK_DENIED)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), third, MembershipChange.KNOCK_DENIED)
val youDeniedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
val youDeniedKnock = formatter.format(youDeniedKnockEvent, false)
assertThat(youDeniedKnock).isEqualTo("You rejected ${someoneContent.userId}'s request to join")
assertThat(youDeniedKnock).isEqualTo("You rejected $third's request to join")
val someoneDeniedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
val someoneDeniedKnock = formatter.format(someoneDeniedKnockEvent, false)
assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected ${someoneContent.userId}'s request to join")
assertThat(someoneDeniedKnock).isEqualTo("$otherName rejected $third's request to join")
val someoneDeniedYourKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = youContent)
val someoneDeniedYourKnock = formatter.format(someoneDeniedYourKnockEvent, false)
@ -478,9 +485,9 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Membership change - None`() {
val otherName = "Someone"
val youContent = RoomMembershipContent(A_USER_ID, MembershipChange.NONE)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), MembershipChange.NONE)
val otherName = "Other"
val youContent = RoomMembershipContent(A_USER_ID, null, MembershipChange.NONE)
val someoneContent = RoomMembershipContent(UserId("@someone_else:domain"), otherName, MembershipChange.NONE)
val youNoneRoomEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = youContent)
val youNoneRoom = formatter.format(youNoneRoomEvent, false)
@ -497,7 +504,7 @@ class DefaultRoomLastMessageFormatterTest {
val otherChanges = arrayOf(MembershipChange.ERROR, MembershipChange.NOT_IMPLEMENTED, null)
val results = otherChanges.map { change ->
val content = RoomMembershipContent(A_USER_ID, change)
val content = RoomMembershipContent(A_USER_ID, null, change)
val event = createRoomEvent(sentByYou = false, senderDisplayName = "Someone", content = content)
val result = formatter.format(event, false)
change to result
@ -513,7 +520,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - avatar`() {
val otherName = "Someone"
val otherName = "Other"
val changedContent = StateContent("", OtherState.RoomAvatar("new_avatar"))
val removedContent = StateContent("", OtherState.RoomAvatar(null))
@ -537,7 +544,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - create`() {
val otherName = "Someone"
val otherName = "Other"
val content = StateContent("", OtherState.RoomCreate)
val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content)
@ -552,7 +559,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - encryption`() {
val otherName = "Someone"
val otherName = "Other"
val content = StateContent("", OtherState.RoomEncryption)
val youCreatedRoomMessage = createRoomEvent(sentByYou = true, senderDisplayName = null, content = content)
@ -567,7 +574,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - room name`() {
val otherName = "Someone"
val otherName = "Other"
val newName = "New name"
val changedContent = StateContent("", OtherState.RoomName(newName))
val removedContent = StateContent("", OtherState.RoomName(null))
@ -592,7 +599,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - third party invite`() {
val otherName = "Someone"
val otherName = "Other"
val inviteeName = "Alice"
val changedContent = StateContent("", OtherState.RoomThirdPartyInvite(inviteeName))
val removedContent = StateContent("", OtherState.RoomThirdPartyInvite(null))
@ -617,7 +624,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Room state change - room topic`() {
val otherName = "Someone"
val otherName = "Other"
val roomTopic = "New topic"
val changedContent = StateContent("", OtherState.RoomTopic(roomTopic))
val removedContent = StateContent("", OtherState.RoomTopic(null))
@ -677,7 +684,7 @@ class DefaultRoomLastMessageFormatterTest {
@Test
@Config(qualifiers = "en")
fun `Profile change - avatar`() {
val otherName = "Someone"
val otherName = "Other"
val changedContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = "old_avatar_url")
val setContent = aProfileChangeMessageContent(avatarUrl = "new_avatar_url", prevAvatarUrl = null)
val removedContent = aProfileChangeMessageContent(avatarUrl = null, prevAvatarUrl = "old_avatar_url")
@ -722,7 +729,7 @@ class DefaultRoomLastMessageFormatterTest {
fun `Profile change - display name`() {
val newDisplayName = "New"
val oldDisplayName = "Old"
val otherName = "Someone"
val otherName = "Other"
val changedContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = oldDisplayName)
val setContent = aProfileChangeMessageContent(displayName = newDisplayName, prevDisplayName = null)
val removedContent = aProfileChangeMessageContent(displayName = null, prevDisplayName = oldDisplayName)

View file

@ -81,5 +81,12 @@ enum class FeatureFlags(
description = "Allow user to search for public rooms in their homeserver",
defaultValue = false,
isFinished = false,
)
),
ShowBlockedUsersDetails(
key = "feature.showBlockedUsersDetails",
title = "Show blocked users details",
description = "Show the name and avatar of blocked users in the blocked users list",
defaultValue = false,
isFinished = false,
),
}

View file

@ -41,6 +41,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.Mentions -> true
FeatureFlags.MarkAsUnread -> true
FeatureFlags.RoomDirectorySearch -> false
FeatureFlags.ShowBlockedUsersDetails -> false
}
} else {
false

View file

@ -38,7 +38,7 @@ class DefaultIndicatorService @Inject constructor(
) : IndicatorService {
@Composable
override fun showRoomListTopBarIndicator(): State<Boolean> {
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
val canVerifySession by sessionVerificationService.needsSessionVerification.collectAsState(initial = false)
val settingChatBackupIndicator = showSettingChatBackupIndicator()
return remember {

View file

@ -38,6 +38,7 @@ dependencies {
implementation(libs.dagger)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.services.analytics.api)
implementation(libs.serialization.json)
api(projects.libraries.sessionStorage.api)
implementation(libs.coroutines.core)

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.matrix.api
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
@ -32,6 +31,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
@ -66,6 +66,7 @@ interface MatrixClient : Closeable {
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit>
suspend fun joinRoom(roomId: RoomId): Result<Unit>
suspend fun joinRoomByIdOrAlias(roomId: RoomId, serverNames: List<String>): Result<Unit>
suspend fun knockRoom(roomId: RoomId): Result<Unit>
fun syncService(): SyncService
fun sessionVerificationService(): SessionVerificationService
@ -102,6 +103,6 @@ interface MatrixClient : Closeable {
suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit>
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId>
suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview>
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<ResolvedRoomAlias>
suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List<String>): Result<RoomPreview>
}

View file

@ -0,0 +1,40 @@
/*
* 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.analytics
import im.vector.app.features.analytics.plan.ViewRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom
fun MatrixRoom.toAnalyticsViewRoom(
trigger: ViewRoom.Trigger? = null,
selectedSpace: MatrixRoom? = null,
viaKeyboard: Boolean? = null,
): ViewRoom {
val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home
return ViewRoom(
isDM = isDirect,
isSpace = isSpace,
trigger = trigger,
activeSpace = activeSpace,
viaKeyboard = viaKeyboard
)
}
private fun MatrixRoom.toActiveSpace(): ViewRoom.ActiveSpace {
return if (isPublic) ViewRoom.ActiveSpace.Public else ViewRoom.ActiveSpace.Private
}

View file

@ -50,4 +50,16 @@ interface EncryptionService {
* Wait for backup upload steady state.
*/
fun waitForBackupUploadSteadyState(): Flow<BackupUploadState>
/**
* Get the public curve25519 key of our own device in base64. This is usually what is
* called the identity key of the device.
*/
suspend fun deviceCurve25519(): String?
/**
* Get the public ed25519 key of our own device. This is usually what is
* called the fingerprint of the device.
*/
suspend fun deviceEd25519(): String?
}

View file

@ -52,6 +52,10 @@ sealed interface NotificationContent {
data class CallInvite(
val senderId: UserId,
) : MessageLike
data class CallNotify(
val senderId: UserId,
val type: CallNotifyType,
) : MessageLike
data object CallHangup : MessageLike
data object CallCandidates : MessageLike
@ -108,3 +112,8 @@ sealed interface NotificationContent {
data object SpaceParent : StateEvent
}
}
enum class CallNotifyType {
RING,
NOTIFY
}

View file

@ -18,5 +18,5 @@ package io.element.android.libraries.matrix.api.pusher
interface PushersService {
suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result<Unit>
suspend fun unsetHttpPusher(): Result<Unit>
suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit>
}

View file

@ -0,0 +1,22 @@
/*
* 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.pusher
data class UnsetHttpPusherData(
val pushKey: String,
val appId: String,
)

View file

@ -27,7 +27,10 @@ import kotlinx.collections.immutable.ImmutableMap
@Immutable
data class MatrixRoomInfo(
val id: RoomId,
/** The room's name from the room state event if received from sync, or one that's been computed otherwise. */
val name: String?,
/** Room name as defined by the room state event only. */
val rawName: String?,
val topic: String?,
val avatarUrl: String?,
val isDirect: Boolean,

View file

@ -0,0 +1,33 @@
/*
* 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.room.alias
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Information about a room, that was resolved from a room alias.
*/
data class ResolvedRoomAlias(
/**
* The room ID that the alias resolved to.
*/
val roomId: RoomId,
/**
* A list of servers that can be used to find the room by its room ID.
*/
val servers: List<String>
)

View file

@ -0,0 +1,28 @@
/*
* 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.room.join
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.core.RoomId
interface JoinRoom {
suspend operator fun invoke(
roomId: RoomId,
serverNames: List<String>,
trigger: JoinedRoom.Trigger,
): Result<Unit>
}

View file

@ -73,6 +73,7 @@ data class UnableToDecryptContent(
data class RoomMembershipContent(
val userId: UserId,
val userDisplayName: String?,
val change: MembershipChange?
) : EventContent

View file

@ -26,12 +26,6 @@ interface SessionVerificationService {
*/
val verificationFlowState: StateFlow<VerificationFlowState>
/**
* The internal service that checks verification can only run after the initial sync.
* This [StateFlow] will notify consumers when the service is ready to be used.
*/
val isReady: StateFlow<Boolean>
/**
* Returns whether the current verification status is either: [SessionVerifiedStatus.Unknown], [SessionVerifiedStatus.NotVerified]
* or [SessionVerifiedStatus.Verified].
@ -39,9 +33,9 @@ interface SessionVerificationService {
val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus>
/**
* Returns whether the current session needs to be verified and the SDK is ready to start the verification.
* Returns whether the current session needs to be verified.
*/
val canVerifySessionFlow: Flow<Boolean>
val needsSessionVerification: Flow<Boolean>
/**
* Request verification of the current session.

View file

@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
@ -39,6 +38,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
@ -443,6 +443,23 @@ class RustMatrixClient(
}
}
override suspend fun joinRoomByIdOrAlias(
roomId: RoomId,
serverNames: List<String>,
): Result<Unit> = withContext(sessionDispatcher) {
runCatching {
client.joinRoomByIdOrAlias(
roomIdOrAlias = roomId.value,
serverNames = serverNames,
).destroy()
try {
awaitRoom(roomId, 10.seconds)
} catch (e: Exception) {
Timber.e(e, "Timeout waiting for the room to be available in the room list")
}
}
}
override suspend fun knockRoom(roomId: RoomId): Result<Unit> {
return Result.failure(NotImplementedError("Not yet implemented"))
}
@ -459,15 +476,22 @@ class RustMatrixClient(
}
}
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId> = withContext(sessionDispatcher) {
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<ResolvedRoomAlias> = withContext(sessionDispatcher) {
runCatching {
client.resolveRoomAlias(roomAlias.value).roomId.let(::RoomId)
val result = client.resolveRoomAlias(roomAlias.value)
ResolvedRoomAlias(
roomId = RoomId(result.roomId),
servers = result.servers,
)
}
}
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = withContext(sessionDispatcher) {
override suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List<String>): Result<RoomPreview> = withContext(sessionDispatcher) {
runCatching {
client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
client.getRoomPreviewFromRoomId(
roomId = roomId.value,
viaServers = serverNames,
).let(RoomPreviewMapper::map)
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.analytics
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom
private fun Long?.toAnalyticsRoomSize(): JoinedRoom.RoomSize {
return when (this) {
null,
2L -> JoinedRoom.RoomSize.Two
in 3..10 -> JoinedRoom.RoomSize.ThreeToTen
in 11..100 -> JoinedRoom.RoomSize.ElevenToOneHundred
in 101..1000 -> JoinedRoom.RoomSize.OneHundredAndOneToAThousand
else -> JoinedRoom.RoomSize.MoreThanAThousand
}
}
fun MatrixRoom.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom {
return JoinedRoom(
isDM = isDirect,
isSpace = isSpace,
roomSize = joinedMemberCount.toAnalyticsRoomSize(),
trigger = trigger
)
}

View file

@ -190,4 +190,12 @@ internal class RustEncryptionService(
it.mapRecoveryException()
}
}
override suspend fun deviceCurve25519(): String? {
return service.curve25519Key()
}
override suspend fun deviceEd25519(): String? {
return service.ed25519Key()
}
}

View file

@ -17,10 +17,12 @@
package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
import org.matrix.rustcomponents.sdk.MessageLikeEventContent
import org.matrix.rustcomponents.sdk.NotifyType
import org.matrix.rustcomponents.sdk.StateEventContent
import org.matrix.rustcomponents.sdk.TimelineEvent
import org.matrix.rustcomponents.sdk.TimelineEventType
@ -79,6 +81,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
MessageLikeEventContent.CallCandidates -> NotificationContent.MessageLike.CallCandidates
MessageLikeEventContent.CallHangup -> NotificationContent.MessageLike.CallHangup
MessageLikeEventContent.CallInvite -> NotificationContent.MessageLike.CallInvite(senderId)
is MessageLikeEventContent.CallNotify -> NotificationContent.MessageLike.CallNotify(senderId, notifyType.map())
MessageLikeEventContent.KeyVerificationAccept -> NotificationContent.MessageLike.KeyVerificationAccept
MessageLikeEventContent.KeyVerificationCancel -> NotificationContent.MessageLike.KeyVerificationCancel
MessageLikeEventContent.KeyVerificationDone -> NotificationContent.MessageLike.KeyVerificationDone
@ -97,3 +100,8 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
}
}
}
private fun NotifyType.map(): CallNotifyType = when (this) {
NotifyType.NOTIFY -> CallNotifyType.NOTIFY
NotifyType.RING -> CallNotifyType.RING
}

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.pushers
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.HttpPusherData
@ -54,8 +55,16 @@ class RustPushersService(
}
}
override suspend fun unsetHttpPusher(): Result<Unit> {
// TODO Missing client API. We need to set the pusher with Kind == null, but we do not have access to this field from the SDK.
return Result.success(Unit)
override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit> {
return withContext(dispatchers.io) {
runCatching {
client.deletePusher(
identifiers = PusherIdentifiers(
pushkey = unsetHttpPusherData.pushKey,
appId = unsetHttpPusherData.appId
),
)
}
}
}
}

View file

@ -39,6 +39,7 @@ class MatrixRoomInfoMapper(
return MatrixRoomInfo(
id = RoomId(it.id),
name = it.displayName,
rawName = it.rawName,
topic = it.topic,
avatarUrl = it.avatarUrl,
isDirect = it.isDirect,

View file

@ -41,7 +41,8 @@ class RoomSyncSubscriber(
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
),
timelineLimit = null
timelineLimit = null,
includeHeroes = true,
)
suspend fun subscribe(roomId: RoomId) = mutex.withLock {

View file

@ -0,0 +1,49 @@
/*
* 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.room.join
import com.squareup.anvil.annotations.ContributesBinding
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.impl.analytics.toAnalyticsJoinedRoom
import io.element.android.services.analytics.api.AnalyticsService
import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultJoinRoom @Inject constructor(
private val client: MatrixClient,
private val analyticsService: AnalyticsService,
) : JoinRoom {
override suspend fun invoke(
roomId: RoomId,
serverNames: List<String>,
trigger: JoinedRoom.Trigger,
): Result<Unit> {
return if (serverNames.isEmpty()) {
client.joinRoom(roomId)
} else {
client.joinRoomByIdOrAlias(roomId, serverNames)
}.onSuccess {
client.getRoom(roomId)?.use { room ->
analyticsService.capture(room.toAnalyticsJoinedRoom(trigger))
}
}
}
}

View file

@ -31,7 +31,7 @@ class MatrixTimelineItemMapper(
private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
) {
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
val uniqueId = timelineItem.uniqueId().toString()
val uniqueId = timelineItem.uniqueId()
val asEvent = it.asEvent()
if (asEvent != null) {
val eventTimelineItem = eventTimelineItemMapper.map(asEvent)

View file

@ -174,23 +174,25 @@ class RustTimeline(
// Use NonCancellable to avoid breaking the timeline when the coroutine is cancelled.
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> = withContext(NonCancellable) {
initLatch.await()
runCatching {
if (!canPaginate(direction)) throw TimelineException.CannotPaginate
updatePaginationStatus(direction) { it.copy(isPaginating = true) }
when (direction) {
Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(PAGINATION_SIZE.toUShort())
Timeline.PaginationDirection.FORWARDS -> inner.focusedPaginateForwards(PAGINATION_SIZE.toUShort())
withContext(dispatcher) {
initLatch.await()
runCatching {
if (!canPaginate(direction)) throw TimelineException.CannotPaginate
updatePaginationStatus(direction) { it.copy(isPaginating = true) }
when (direction) {
Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(PAGINATION_SIZE.toUShort())
Timeline.PaginationDirection.FORWARDS -> inner.focusedPaginateForwards(PAGINATION_SIZE.toUShort())
}
}.onFailure { error ->
updatePaginationStatus(direction) { it.copy(isPaginating = false) }
if (error is TimelineException.CannotPaginate) {
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
} else {
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
}
}.onSuccess { hasReachedEnd ->
updatePaginationStatus(direction) { it.copy(isPaginating = false, hasMoreToLoad = !hasReachedEnd) }
}
}.onFailure { error ->
updatePaginationStatus(direction) { it.copy(isPaginating = false) }
if (error is TimelineException.CannotPaginate) {
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
} else {
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
}
}.onSuccess { hasReachedEnd ->
updatePaginationStatus(direction) { it.copy(isPaginating = false, hasMoreToLoad = !hasReachedEnd) }
}
}
@ -214,18 +216,20 @@ class RustTimeline(
backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(),
forwardPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(),
) { timelineItems, hasMoreToLoadBackward, hasMoreToLoadForward ->
timelineItems
.let { items -> encryptedHistoryPostProcessor.process(items) }
.let { items ->
roomBeginningPostProcessor.process(
items = items,
isDm = matrixRoom.isDm,
hasMoreToLoadBackwards = hasMoreToLoadBackward
)
}
.let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) }
// Keep lastForwardIndicatorsPostProcessor last
.let { items -> lastForwardIndicatorsPostProcessor.process(items) }
withContext(dispatcher) {
timelineItems
.let { items -> encryptedHistoryPostProcessor.process(items) }
.let { items ->
roomBeginningPostProcessor.process(
items = items,
isDm = matrixRoom.isDm,
hasMoreToLoadBackwards = hasMoreToLoadBackward
)
}
.let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) }
// Keep lastForwardIndicatorsPostProcessor last
.let { items -> lastForwardIndicatorsPostProcessor.process(items) }
}
}
override fun close() {

View file

@ -88,8 +88,9 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
}
is TimelineItemContentKind.RoomMembership -> {
RoomMembershipContent(
UserId(kind.userId),
kind.change?.map()
userId = UserId(kind.userId),
userDisplayName = kind.userDisplayName,
change = kind.change?.map()
)
}
is TimelineItemContentKind.State -> {

View file

@ -30,8 +30,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -80,10 +80,14 @@ class RustSessionVerificationService(
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
override val isReady = isSyncServiceReady.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
/**
* The internal service that checks verification can only run after the initial sync.
* This [StateFlow] will notify consumers when the service is ready to be used.
*/
private val isReady = isSyncServiceReady.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
override val canVerifySessionFlow = combine(sessionVerifiedStatus, isReady) { verificationStatus, isReady ->
isReady && verificationStatus == SessionVerifiedStatus.NotVerified
override val needsSessionVerification = sessionVerifiedStatus.map { verificationStatus ->
verificationStatus == SessionVerifiedStatus.NotVerified
}
init {

View file

@ -0,0 +1,103 @@
/*
* 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.room.join
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.impl.analytics.toAnalyticsJoinedRoom
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultJoinRoomTest {
@Test
fun `when there is no server names, the classic join room API is used`() = runTest {
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(Unit) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomId, _: List<String> -> Result.success(Unit) }
val roomResult = FakeMatrixRoom()
val aTrigger = JoinedRoom.Trigger.MobilePermalink
val client: MatrixClient = FakeMatrixClient().also {
it.joinRoomLambda = joinRoomLambda
it.joinRoomByIdOrAliasLambda = joinRoomByIdOrAliasLambda
it.givenGetRoomResult(
roomId = A_ROOM_ID,
result = roomResult
)
}
val analyticsService = FakeAnalyticsService()
val sut = DefaultJoinRoom(
client = client,
analyticsService = analyticsService,
)
sut.invoke(A_ROOM_ID, emptyList(), aTrigger)
joinRoomByIdOrAliasLambda
.assertions()
.isNeverCalled()
joinRoomLambda
.assertions()
.isCalledOnce()
.with(
value(A_ROOM_ID)
)
assertThat(analyticsService.capturedEvents).containsExactly(
roomResult.toAnalyticsJoinedRoom(aTrigger)
)
}
@Test
fun `when server names are available, joinRoomByIdOrAlias API is used`() = runTest {
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(Unit) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomId, _: List<String> -> Result.success(Unit) }
val roomResult = FakeMatrixRoom()
val aTrigger = JoinedRoom.Trigger.MobilePermalink
val client: MatrixClient = FakeMatrixClient().also {
it.joinRoomLambda = joinRoomLambda
it.joinRoomByIdOrAliasLambda = joinRoomByIdOrAliasLambda
it.givenGetRoomResult(
roomId = A_ROOM_ID,
result = roomResult
)
}
val analyticsService = FakeAnalyticsService()
val sut = DefaultJoinRoom(
client = client,
analyticsService = analyticsService,
)
sut.invoke(A_ROOM_ID, A_SERVER_LIST, aTrigger)
joinRoomByIdOrAliasLambda
.assertions()
.isCalledOnce()
.with(
value(A_ROOM_ID),
value(A_SERVER_LIST)
)
joinRoomLambda
.assertions()
.isNeverCalled()
assertThat(analyticsService.capturedEvents).containsExactly(
roomResult.toAnalyticsJoinedRoom(aTrigger)
)
}
}

View file

@ -34,7 +34,7 @@ class RoomBeginningPostProcessorTest {
fun `processor removes room creation event and self-join event from DM timeline`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
@ -44,13 +44,13 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor removes room creation event and self-join event from DM timeline even if they're not the first items`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val expected = listOf(
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
)
val processor = RoomBeginningPostProcessor()
@ -62,7 +62,7 @@ class RoomBeginningPostProcessorTest {
fun `processor will add beginning of room item if it's not a DM`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
@ -85,7 +85,7 @@ class RoomBeginningPostProcessorTest {
fun `processor won't remove items if it's not at the start of the timeline`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
@ -95,7 +95,7 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor won't remove the first member join event if it can't find the room creation event`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
@ -106,7 +106,7 @@ class RoomBeginningPostProcessorTest {
fun `processor won't remove the first member join event if it's not from the room creator`() {
val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)

View file

@ -27,6 +27,7 @@ dependencies {
api(projects.libraries.matrix.api)
api(libs.coroutines.core)
implementation(libs.coroutines.test)
implementation(projects.services.analytics.api)
implementation(projects.tests.testutils)
implementation(libs.kotlinx.collections.immutable)
}

View file

@ -20,7 +20,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
@ -33,6 +32,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
@ -40,7 +40,7 @@ import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.pushers.FakePushersService
@ -53,7 +53,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
@ -67,7 +66,7 @@ class FakeMatrixClient(
private val userDisplayName: String? = A_USER_NAME,
private val userAvatarUrl: String? = AN_AVATAR_URL,
override val roomListService: RoomListService = FakeRoomListService(),
override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(),
override val mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private val pushersService: FakePushersService = FakePushersService(),
private val notificationService: FakeNotificationService = FakeNotificationService(),
@ -76,8 +75,8 @@ class FakeMatrixClient(
private val encryptionService: FakeEncryptionService = FakeEncryptionService(),
private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(),
private val accountManagementUrlString: Result<String?> = Result.success(null),
private val resolveRoomAliasResult: (RoomAlias) -> Result<RoomId> = { Result.success(A_ROOM_ID) },
private val getRoomPreviewResult: (RoomIdOrAlias) -> Result<RoomPreview> = { Result.failure(AN_EXCEPTION) },
private val resolveRoomAliasResult: (RoomAlias) -> Result<ResolvedRoomAlias> = { Result.success(ResolvedRoomAlias(A_ROOM_ID, emptyList())) },
private val getRoomPreviewFromRoomIdResult: (RoomId, List<String>) -> Result<RoomPreview> = { _, _ -> Result.failure(AN_EXCEPTION) },
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
private set
@ -95,7 +94,6 @@ class FakeMatrixClient(
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var findDmResult: RoomId? = A_ROOM_ID
private var logoutFailure: Throwable? = null
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
@ -106,12 +104,18 @@ class FakeMatrixClient(
var joinRoomLambda: (RoomId) -> Result<Unit> = {
Result.success(Unit)
}
var joinRoomByIdOrAliasLambda: (RoomId, List<String>) -> Result<Unit> = { _, _ ->
Result.success(Unit)
}
var knockRoomLambda: (RoomId) -> Result<Unit> = {
Result.success(Unit)
}
var getRoomInfoFlowLambda = { _: RoomId ->
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
}
var logoutLambda: (Boolean) -> String? = {
null
}
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
return getRoomResults[roomId]
@ -156,12 +160,8 @@ class FakeMatrixClient(
override suspend fun clearCache() {
}
override suspend fun logout(ignoreSdkError: Boolean): String? {
delay(100)
if (ignoreSdkError.not()) {
logoutFailure?.let { throw it }
}
return null
override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask {
return logoutLambda(ignoreSdkError)
}
override fun close() = Unit
@ -201,6 +201,10 @@ class FakeMatrixClient(
override suspend fun joinRoom(roomId: RoomId): Result<Unit> = joinRoomLambda(roomId)
override suspend fun joinRoomByIdOrAlias(roomId: RoomId, serverNames: List<String>): Result<Unit> {
return joinRoomByIdOrAliasLambda(roomId, serverNames)
}
override suspend fun knockRoom(roomId: RoomId): Result<Unit> = knockRoomLambda(roomId)
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
@ -221,10 +225,6 @@ class FakeMatrixClient(
// Mocks
fun givenLogoutError(failure: Throwable?) {
logoutFailure = failure
}
fun givenCreateRoomResult(result: Result<RoomId>) {
createRoomResult = result
}
@ -285,12 +285,12 @@ class FakeMatrixClient(
return Result.success(Unit)
}
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId> = simulateLongTask {
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<ResolvedRoomAlias> = simulateLongTask {
resolveRoomAliasResult(roomAlias)
}
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = simulateLongTask {
getRoomPreviewResult(roomIdOrAlias)
override suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List<String>) = simulateLongTask {
getRoomPreviewFromRoomIdResult(roomId, serverNames)
}
override suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>> {

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
const val A_USER_NAME = "alice"
const val A_PASSWORD = "password"
const val A_SECRET = "secret"
val A_USER_ID = UserId("@alice:server.org")
val A_USER_ID_2 = UserId("@bob:server.org")
@ -56,6 +57,7 @@ val A_TRANSACTION_ID = TransactionId("aTransactionId")
const val A_UNIQUE_ID = "aUniqueId"
const val A_ROOM_NAME = "A room name"
const val A_ROOM_RAW_NAME = "A room raw name"
const val A_MESSAGE = "Hello world!"
const val A_REPLY = "OK, I'll be there!"
const val ANOTHER_MESSAGE = "Hello universe!"
@ -76,3 +78,5 @@ val A_THROWABLE = Throwable(A_FAILURE_REASON)
val AN_EXCEPTION = Exception(A_FAILURE_REASON)
const val A_RECOVERY_KEY = "1234 5678"
val A_SERVER_LIST = listOf("server1", "server2")

View file

@ -31,7 +31,9 @@ import kotlinx.coroutines.flow.flowOf
val A_OIDC_DATA = OidcDetails(url = "a-url")
class FakeAuthenticationService : MatrixAuthenticationService {
class FakeMatrixAuthenticationService(
private val matrixClientResult: ((SessionId) -> Result<MatrixClient>)? = null
) : MatrixAuthenticationService {
private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private var oidcError: Throwable? = null
private var oidcCancelError: Throwable? = null
@ -39,15 +41,18 @@ class FakeAuthenticationService : MatrixAuthenticationService {
private var changeServerError: Throwable? = null
private var matrixClient: MatrixClient? = null
var getLatestSessionIdLambda: (() -> SessionId?) = { null }
override fun loggedInStateFlow(): Flow<LoggedInState> {
return flowOf(LoggedInState.NotLoggedIn)
}
override suspend fun getLatestSessionId(): SessionId? {
return null
}
override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda()
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> {
if (matrixClientResult != null) {
return matrixClientResult.invoke(sessionId)
}
return if (matrixClient != null) {
Result.success(matrixClient!!)
} else {

View file

@ -28,7 +28,7 @@ fun aBuildMeta(
applicationId: String = "",
lowPrivacyLoggingEnabled: Boolean = true,
versionName: String = "",
versionCode: Int = 0,
versionCode: Long = 0,
gitRevision: String = "",
gitBranchName: String = "",
flavorDescription: String = "",

View file

@ -39,6 +39,9 @@ class FakeEncryptionService : EncryptionService {
private var enableBackupsFailure: Exception? = null
private var curve25519: String? = null
private var ed25519: String? = null
fun givenEnableBackupsFailure(exception: Exception?) {
enableBackupsFailure = exception
}
@ -94,6 +97,15 @@ class FakeEncryptionService : EncryptionService {
return waitForBackupUploadSteadyStateFlow
}
fun givenDeviceKeys(curve25519: String?, ed25519: String?) {
this.curve25519 = curve25519
this.ed25519 = ed25519
}
override suspend fun deviceCurve25519(): String? = curve25519
override suspend fun deviceEd25519(): String? = ed25519
suspend fun emitBackupState(state: BackupState) {
backupStateStateFlow.emit(state)
}

View file

@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.tests.testutils.simulateLongTask
class FakeMediaLoader : MatrixMediaLoader {
class FakeMatrixMediaLoader : MatrixMediaLoader {
var shouldFail = false
var path: String = ""

View file

@ -20,9 +20,9 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
class FakePermalinkBuilder(
private val result: () -> Result<String> = { Result.failure(Exception("Not implemented")) }
private val result: (UserId) -> Result<String> = { Result.failure(Exception("Not implemented")) }
) : PermalinkBuilder {
override fun permalinkForUser(userId: UserId): Result<String> {
return result()
return result(userId)
}
}

View file

@ -18,9 +18,10 @@ package io.element.android.libraries.matrix.test.permalink
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.tests.testutils.lambda.lambdaError
class FakePermalinkParser(
private var result: () -> PermalinkData = { TODO("Not implemented") }
private var result: () -> PermalinkData = { lambdaError() }
) : PermalinkParser {
fun givenResult(result: PermalinkData) {
this.result = { result }

View file

@ -18,8 +18,13 @@ package io.element.android.libraries.matrix.test.pushers
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import io.element.android.tests.testutils.lambda.lambdaError
class FakePushersService : PushersService {
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit)
override suspend fun unsetHttpPusher(): Result<Unit> = Result.success(Unit)
class FakePushersService(
private val setHttpPusherResult: (SetHttpPusherData) -> Result<Unit> = { lambdaError() },
private val unsetHttpPusherResult: (UnsetHttpPusherData) -> Result<Unit> = { lambdaError() },
) : PushersService {
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = setHttpPusherResult(setHttpPusherData)
override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit> = unsetHttpPusherResult(unsetHttpPusherData)
}

View file

@ -55,7 +55,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
@ -125,7 +125,7 @@ class FakeMatrixRoom(
private var endPollResult = Result.success(Unit)
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io")
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver())
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeMatrixWidgetDriver())
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
private var setIsFavoriteResult = Result.success(Unit)
@ -735,6 +735,7 @@ data class EndPollInvocation(
fun aRoomInfo(
id: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME,
rawName: String? = name,
topic: String? = "A topic",
avatarUrl: String? = AN_AVATAR_URL,
isDirect: Boolean = false,
@ -759,6 +760,7 @@ fun aRoomInfo(
) = MatrixRoomInfo(
id = id,
name = name,
rawName = rawName,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,

View file

@ -0,0 +1,34 @@
/*
* 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.test.room.join
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.tests.testutils.simulateLongTask
class FakeJoinRoom(
var lambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit>
) : JoinRoom {
override suspend fun invoke(
roomId: RoomId,
serverNames: List<String>,
trigger: JoinedRoom.Trigger,
): Result<Unit> = simulateLongTask {
lambda(roomId, serverNames, trigger)
}
}

View file

@ -25,17 +25,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeSessionVerificationService : SessionVerificationService {
private val _isReady = MutableStateFlow(false)
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
private var _canVerifySessionFlow = MutableStateFlow(true)
private var _needsSessionVerification = MutableStateFlow(true)
var shouldFail = false
override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
override val isReady: StateFlow<Boolean> = _isReady
override val needsSessionVerification: Flow<Boolean> = _needsSessionVerification
override suspend fun requestVerification() {
if (!shouldFail) {
@ -85,12 +82,8 @@ class FakeSessionVerificationService : SessionVerificationService {
_verificationFlowState.value = state
}
fun givenCanVerifySession(canVerify: Boolean) {
_canVerifySessionFlow.value = canVerify
}
fun givenIsReady(value: Boolean) {
_isReady.value = value
fun givenNeedsSessionVerification(needsVerification: Boolean) {
_needsSessionVerification.value = needsVerification
}
override suspend fun reset() {

View file

@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import kotlinx.coroutines.flow.MutableSharedFlow
import java.util.UUID
class FakeWidgetDriver(
class FakeMatrixWidgetDriver(
override val id: String = UUID.randomUUID().toString(),
) : MatrixWidgetDriver {
private val _sentMessages = mutableListOf<String>()

View file

@ -39,6 +39,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.core)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.testtags)
implementation(libs.coil.compose)
implementation(libs.coil.gif)
implementation(libs.jsoup)

View file

@ -14,25 +14,23 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalMaterialApi::class)
@file:Suppress("UsingMaterialAndMaterial3Libraries")
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.libraries.matrix.ui.components
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.list.ListItemContent
@ -41,49 +39,60 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.hide
import io.element.android.libraries.matrix.ui.media.AvatarAction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AvatarActionBottomSheet(
actions: ImmutableList<AvatarAction>,
modalBottomSheetState: ModalBottomSheetState,
onActionSelected: (action: AvatarAction) -> Unit,
isVisible: Boolean,
onSelectAction: (action: AvatarAction) -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
fun onItemActionClicked(itemAction: AvatarAction) {
onActionSelected(itemAction)
coroutineScope.launch {
modalBottomSheetState.hide()
}
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
BackHandler(enabled = isVisible) {
sheetState.hide(coroutineScope, then = { onDismiss() })
}
ModalBottomSheetLayout(
modifier = modifier,
sheetState = modalBottomSheetState,
displayHandle = true,
sheetContent = {
fun onItemActionClick(itemAction: AvatarAction) {
onSelectAction(itemAction)
sheetState.hide(coroutineScope, then = { onDismiss() })
}
if (isVisible) {
ModalBottomSheet(
onDismissRequest = {
sheetState.hide(coroutineScope, then = { onDismiss() })
},
modifier = modifier,
sheetState = sheetState,
) {
AvatarActionBottomSheetContent(
actions = actions,
onActionClicked = ::onItemActionClicked,
onActionClick = ::onItemActionClick,
modifier = Modifier
.navigationBarsPadding()
.imePadding()
)
}
)
}
}
@Composable
private fun AvatarActionBottomSheetContent(
actions: ImmutableList<AvatarAction>,
modifier: Modifier = Modifier,
onActionClicked: (AvatarAction) -> Unit = { },
onActionClick: (AvatarAction) -> Unit = { },
) {
LazyColumn(
modifier = modifier.fillMaxWidth()
@ -92,7 +101,7 @@ private fun AvatarActionBottomSheetContent(
items = actions,
) { action ->
ListItem(
modifier = Modifier.clickable { onActionClicked(action) },
modifier = Modifier.clickable { onActionClick(action) },
headlineContent = {
Text(
text = stringResource(action.titleResId),
@ -115,10 +124,8 @@ private fun AvatarActionBottomSheetContent(
internal fun AvatarActionBottomSheetPreview() = ElementPreview {
AvatarActionBottomSheet(
actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove),
modalBottomSheetState = ModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded,
density = LocalDensity.current,
),
onActionSelected = { },
isVisible = true,
onSelectAction = { },
onDismiss = { },
)
}

View file

@ -33,40 +33,48 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
@Composable
fun EditableAvatarView(
userId: String?,
matrixId: String,
displayName: String?,
avatarUrl: Uri?,
avatarSize: AvatarSize,
onAvatarClicked: () -> Unit,
onAvatarClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.size(avatarSize.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
onClick = onAvatarClicked,
onClick = onAvatarClick,
indication = rememberRipple(bounded = false),
)
.testTag(TestTags.editAvatar)
) {
when (avatarUrl?.scheme) {
null, "mxc" -> {
userId?.let {
Avatar(
avatarData = AvatarData(it, displayName, avatarUrl?.toString(), size = avatarSize),
modifier = Modifier.fillMaxSize(),
)
}
Avatar(
avatarData = AvatarData(matrixId, displayName, avatarUrl?.toString(), size = avatarSize),
modifier = Modifier.fillMaxSize(),
)
}
else -> {
UnsavedAvatar(
@ -94,3 +102,26 @@ fun EditableAvatarView(
}
}
}
@PreviewsDayNight
@Composable
internal fun EditableAvatarViewPreview(
@PreviewParameter(EditableAvatarViewUriProvider::class) uri: Uri?
) = ElementPreview {
EditableAvatarView(
matrixId = "id",
displayName = "A room",
avatarUrl = uri,
avatarSize = AvatarSize.EditRoomDetails,
onAvatarClick = {},
)
}
open class EditableAvatarViewUriProvider : PreviewParameterProvider<Uri?> {
override val values: Sequence<Uri?>
get() = sequenceOf(
null,
Uri.parse("mxc://matrix.org/123456"),
Uri.parse("https://example.com/avatar.jpg"),
)
}

View file

@ -50,7 +50,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun SelectedRoom(
roomSummary: RoomSummaryDetails,
onRoomRemoved: (RoomSummaryDetails) -> Unit,
onRemoveRoom: (RoomSummaryDetails) -> Unit,
modifier: Modifier = Modifier,
) {
Box(
@ -78,7 +78,7 @@ fun SelectedRoom(
.clickable(
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() },
onClick = { onRoomRemoved(roomSummary) }
onClick = { onRemoveRoom(roomSummary) }
),
) {
Icon(
@ -98,6 +98,6 @@ internal fun SelectedRoomPreview(
) = ElementPreview {
SelectedRoom(
roomSummary = roomSummaryDetails,
onRoomRemoved = {},
onRemoveRoom = {},
)
}

View file

@ -53,7 +53,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun SelectedUser(
matrixUser: MatrixUser,
canRemove: Boolean,
onUserRemoved: (MatrixUser) -> Unit,
onUserRemove: (MatrixUser) -> Unit,
modifier: Modifier = Modifier,
) {
Box(
@ -83,7 +83,7 @@ fun SelectedUser(
.clickable(
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() },
onClick = { onUserRemoved(matrixUser) }
onClick = { onUserRemove(matrixUser) }
),
) {
Icon(
@ -103,7 +103,7 @@ internal fun SelectedUserPreview() = ElementPreview {
SelectedUser(
aMatrixUser(displayName = "John Doe"),
canRemove = true,
onUserRemoved = {},
onUserRemove = {},
)
}
@ -113,6 +113,6 @@ internal fun SelectedUserCannotRemovePreview() = ElementPreview {
SelectedUser(
aMatrixUser(),
canRemove = false,
onUserRemoved = {},
onUserRemove = {},
)
}

View file

@ -48,7 +48,7 @@ import kotlin.math.floor
@Composable
fun SelectedUsersRowList(
selectedUsers: ImmutableList<MatrixUser>,
onUserRemoved: (MatrixUser) -> Unit,
onUserRemove: (MatrixUser) -> Unit,
modifier: Modifier = Modifier,
autoScroll: Boolean = false,
canDeselect: (MatrixUser) -> Boolean = { true },
@ -112,7 +112,7 @@ fun SelectedUsersRowList(
SelectedUser(
matrixUser = selectedUser,
canRemove = canDeselect(selectedUser),
onUserRemoved = onUserRemoved,
onUserRemove = onUserRemove,
)
},
measurePolicy = { measurables, constraints ->
@ -137,7 +137,7 @@ internal fun SelectedUsersRowListPreview() = ElementPreview {
// Two users that will be visible with no scrolling
SelectedUsersRowList(
selectedUsers = aMatrixUserList().take(2).toImmutableList(),
onUserRemoved = {},
onUserRemove = {},
modifier = Modifier
.width(200.dp)
.border(1.dp, Color.Red)
@ -147,7 +147,7 @@ internal fun SelectedUsersRowListPreview() = ElementPreview {
for (i in 0..5) {
SelectedUsersRowList(
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
onUserRemoved = {},
onUserRemove = {},
modifier = Modifier
.width((200 + i * 20).dp)
.border(1.dp, Color.Red)

View file

@ -62,3 +62,21 @@ fun MatrixRoom.isOwnUserAdmin(): Boolean {
val powerLevel = roomInfo?.userPowerLevels?.get(sessionId) ?: 0L
return RoomMember.Role.forPowerLevel(powerLevel) == RoomMember.Role.ADMIN
}
@Composable
fun MatrixRoom.rawName(): String? {
val roomInfo by roomInfoFlow.collectAsState(initial = null)
return roomInfo?.rawName
}
@Composable
fun MatrixRoom.topic(): String? {
val roomInfo by roomInfoFlow.collectAsState(initial = null)
return roomInfo?.topic
}
@Composable
fun MatrixRoom.avatarUrl(): String? {
val roomInfo by roomInfoFlow.collectAsState(initial = null)
return roomInfo?.avatarUrl
}

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="screen_invites_invited_you">"%1$s (%2$s) მოგიწვიათ"</string>
</resources>

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="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>
</resources>

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="screen_invites_invited_you">"%1$s %2$s邀请了你"</string>
</resources>

View file

@ -58,17 +58,16 @@ class AndroidMediaPreProcessorTest {
val data = result.getOrThrow()
assertThat(data.file.path).endsWith("image.png")
val info = data as MediaUploadInfo.Image
// Computing thumbnailFile is failing with Robolectric
assertThat(info.thumbnailFile).isNull()
assertThat(info.thumbnailFile).isNotNull()
assertThat(info.imageInfo).isEqualTo(
ImageInfo(
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Png,
size = 114_867,
thumbnailInfo = null,
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4567),
thumbnailSource = null,
blurhash = null,
blurhash = "K13]7q%zWC00R4of%\$baad"
)
)
assertThat(file.exists()).isTrue()
@ -88,7 +87,6 @@ class AndroidMediaPreProcessorTest {
val data = result.getOrThrow()
assertThat(data.file.path).endsWith("image.png")
val info = data as MediaUploadInfo.Image
// Computing thumbnailFile is failing with Robolectric
assertThat(info.thumbnailFile).isNull()
assertThat(info.imageInfo).isEqualTo(
ImageInfo(

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