Merge branch 'develop' into renovate/org.maplibre.gl-android-sdk-11.x
This commit is contained in:
commit
232c8de702
1249 changed files with 18041 additions and 8127 deletions
|
|
@ -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
|
||||
// ==============================================================================================================
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ internal fun AsyncActionViewPreview(
|
|||
ConfirmationDialog(
|
||||
title = "Confirmation",
|
||||
content = "Are you sure?",
|
||||
onSubmitClicked = {},
|
||||
onSubmitClick = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@ private fun RetryDialogContent(
|
|||
title = title,
|
||||
content = content,
|
||||
submitText = retryText,
|
||||
onSubmitClicked = onRetry,
|
||||
onSubmitClick = onRetry,
|
||||
cancelText = dismissText,
|
||||
onCancelClicked = onDismiss,
|
||||
onCancelClick = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
|||
FeatureFlags.Mentions -> true
|
||||
FeatureFlags.MarkAsUnread -> true
|
||||
FeatureFlags.RoomDirectorySearch -> false
|
||||
FeatureFlags.ShowBlockedUsersDetails -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -73,6 +73,7 @@ data class UnableToDecryptContent(
|
|||
|
||||
data class RoomMembershipContent(
|
||||
val userId: UserId,
|
||||
val userDisplayName: String?,
|
||||
val change: MembershipChange?
|
||||
) : EventContent
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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 = "",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue