Merge branch 'develop' into feature/fga/timeline-multi-forward

This commit is contained in:
ganfra
2026-10-05 17:54:00 +02:00
335 changed files with 2657 additions and 1194 deletions
@@ -9,8 +9,11 @@
package io.element.android.libraries.androidutils.clipboard
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.os.PersistableBundle
import androidx.core.content.getSystemService
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
@@ -24,7 +27,30 @@ class AndroidClipboardHelper(
) : ClipboardHelper {
private val clipboardManager = requireNotNull(context.getSystemService<ClipboardManager>())
override fun copyPlainText(text: String) {
clipboardManager.setPrimaryClip(ClipData.newPlainText("", text))
override fun copyPlainText(
text: String,
isSensitive: Boolean,
onSuccessOnOldDevice: () -> Unit,
) {
val clipData = ClipData.newPlainText("", text)
if (isSensitive) {
clipData.description.extras = PersistableBundle().apply {
putBoolean(EXTRA_IS_SENSITIVE, true)
}
}
clipboardManager.setPrimaryClip(clipData)
// On Android 13+, the system already displays a confirmation when content is copied to the clipboard.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
onSuccessOnOldDevice()
}
}
private companion object {
// The constant only exists from API 33, but the documentation recommends using its value on older versions too.
val EXTRA_IS_SENSITIVE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ClipDescription.EXTRA_IS_SENSITIVE
} else {
"android.content.extra.IS_SENSITIVE"
}
}
}
@@ -8,9 +8,18 @@
package io.element.android.libraries.androidutils.clipboard
/**
* Wrapper class for handling clipboard operations so it can be used in JVM environments.
*/
interface ClipboardHelper {
fun copyPlainText(text: String)
/**
* Copy a text to the clipboard.
*
* @param text the text to copy
* @param isSensitive if true, the clip is flagged so the system clipboard preview (Android 13+) and keyboards hide its content.
* @param onSuccessOnOldDevice invoked after the copy only on Android versions older than 13, since newer versions already
* display a confirmation. Can be used to render a Toast or a Snackbar.
*/
fun copyPlainText(
text: String,
isSensitive: Boolean = false,
onSuccessOnOldDevice: () -> Unit,
)
}
@@ -8,10 +8,21 @@
package io.element.android.libraries.androidutils.clipboard
class FakeClipboardHelper : ClipboardHelper {
class FakeClipboardHelper(
private val isOldDevice: Boolean = true,
) : ClipboardHelper {
var clipboardContents: Any? = null
var isSensitive: Boolean? = null
override fun copyPlainText(text: String) {
override fun copyPlainText(
text: String,
isSensitive: Boolean,
onSuccessOnOldDevice: () -> Unit,
) {
clipboardContents = text
this.isSensitive = isSensitive
if (isOldDevice) {
onSuccessOnOldDevice()
}
}
}
@@ -1,23 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.androidutils.system
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.core.content.getSystemService
class CopyToClipboardUseCase(
private val context: Context,
) {
fun execute(text: CharSequence) {
context.getSystemService<ClipboardManager>()
?.setPrimaryClip(ClipData.newPlainText("", text))
}
}
@@ -50,25 +50,6 @@ fun Context.getVersionCodeFromManifest(): Long {
)
}
// ==============================================================================================================
// Clipboard helper
// ==============================================================================================================
/**
* Copy a text to the clipboard, and display a Toast when done.
*
* @receiver the context
* @param text the text to copy
* @param toastMessage content of the toast message as a String resource. Null for no toast
*/
fun Context.copyToClipboard(
text: CharSequence,
toastMessage: String? = null
) {
CopyToClipboardUseCase(this).execute(text)
toastMessage?.let { toast(it) }
}
/**
* Shows notification settings for the current app.
* In android O will directly opens the notification settings, in lower version it will show the App settings
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.androidutils.toast
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.system.toast
import io.element.android.libraries.di.annotations.ApplicationContext
@ContributesBinding(AppScope::class)
class AndroidToastHelper(
@ApplicationContext private val context: Context,
) : ToastHelper {
override fun show(resId: Int) {
context.toast(resId)
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.androidutils.toast
class FakeToastHelper : ToastHelper {
val shownToasts = mutableListOf<Int>()
override fun show(resId: Int) {
shownToasts.add(resId)
}
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.androidutils.toast
import androidx.annotation.StringRes
interface ToastHelper {
fun show(@StringRes resId: Int)
}
@@ -8,39 +8,43 @@
package io.element.android.libraries.designsystem.atomic.atoms
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.Text
private const val MAX_COUNT = 99
private const val MAX_COUNT_STRING = "+$MAX_COUNT"
private const val MAX_COUNT_STRING = "$MAX_COUNT+"
/**
* A counter atom that displays a number in a circle.
* Figma link : https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2805-2649&m=dev
*
* @param count The number to display. If the number is greater than [MAX_COUNT], the counter will display [MAX_COUNT_STRING].
* If the number is less than 1, the counter will not be displayed.
* If the number is less than 1, just a dot will be displayed instead.
* @param modifier The modifier to apply to this layout.
* @param containerColor The background color of the counter. When null, uses [isCritical] to pick a default.
* @param contentColor The text color inside the counter. When null, uses [textOnSolidPrimary].
* @param contentColor The text color inside the counter. When null, uses [SemanticColors.textOnSolidPrimary].
* @param contentPadding The padding to apply to the text inside the counter.
* @param textStyle The style to apply to the text inside the counter.
* @param dotSize The size of the dot when [count] is less than 1. It defaults to `12.0.dp`.
* @param isCritical If true, the counter will use a critical color scheme, otherwise it will use an accent color scheme.
* Only used when [containerColor] is null.
*/
@@ -50,41 +54,51 @@ fun CounterAtom(
modifier: Modifier = Modifier,
containerColor: Color? = null,
contentColor: Color? = null,
contentPadding: PaddingValues = PaddingValues.Zero,
textStyle: TextStyle = CounterAtomDefaults.textStyle,
dotSize: Dp = 12.0.dp,
isCritical: Boolean = false,
) {
if (count < 1) return
val countAsText = when (count) {
in 0..MAX_COUNT -> count.toString()
in Int.MIN_VALUE..0 -> null
in 1..MAX_COUNT -> count.toString()
else -> MAX_COUNT_STRING
}
val textMeasurer = rememberTextMeasurer()
// Measure the maximum count string size
val textLayoutResult = textMeasurer.measure(
text = MAX_COUNT_STRING,
style = textStyle
)
val textSize = textLayoutResult.size
val squareSize = maxOf(textSize.width, textSize.height)
Box(
modifier = modifier
.size(squareSize.toDp() + 1.dp)
.clip(CircleShape)
.background(
containerColor ?: if (isCritical) {
ElementTheme.colors.iconCriticalPrimary
} else {
ElementTheme.colors.iconAccentPrimary
}
)
) {
Text(
modifier = Modifier.align(Alignment.Center),
val minSingleDigitSize = if (countAsText?.length == 1) {
val textMeasurer = rememberTextMeasurer()
// Measure the maximum count string size
val textLayoutResult = textMeasurer.measure(
text = countAsText,
style = textStyle,
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
style = textStyle
)
val textSize = textLayoutResult.size
maxOf(textSize.width, textSize.height)
} else {
0
}
androidx.compose.material3.Badge(
modifier = modifier.then(
if (countAsText == null) Modifier.sizeIn(minWidth = dotSize, minHeight = dotSize) else Modifier
),
containerColor = containerColor ?: if (isCritical) ElementTheme.colors.iconCriticalPrimary else ElementTheme.colors.iconAccentPrimary,
contentColor = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
content = countAsText?.let {
@Composable {
Text(
modifier = Modifier.sizeIn(
minWidth = (minSingleDigitSize.toDp() - 8.dp).coerceAtLeast(0.dp),
minHeight = minSingleDigitSize.toDp()
).padding(contentPadding),
text = countAsText,
style = textStyle,
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
textAlign = TextAlign.Center,
)
}
}
)
}
object CounterAtomDefaults {
@@ -95,7 +109,7 @@ object CounterAtomDefaults {
@PreviewsDayNight
@Composable
internal fun CounterAtomPreview() = ElementPreview {
Column(verticalArrangement = spacedBy(2.dp)) {
Row(horizontalArrangement = spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
CounterAtom(count = 0)
CounterAtom(count = 4)
CounterAtom(count = 99)
@@ -9,15 +9,16 @@
package io.element.android.libraries.designsystem.atomic.atoms
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
@@ -37,35 +38,36 @@ fun UnreadIndicatorAtom(
isVisible: Boolean = true,
contentDescription: String? = null,
border: BorderStroke? = null,
contentPadding: PaddingValues = PaddingValues.Zero,
) {
when {
!isVisible -> Spacer(modifier = modifier.size(size))
count != null && count >= 1 -> CounterAtom(
count = count.toInt(),
modifier = modifier
.semantics {
contentDescription?.let { this.contentDescription = it }
}
.then(if (border != null) Modifier.border(border, CircleShape) else Modifier),
containerColor = color,
contentColor = ElementTheme.colors.bgCanvasDefault,
textStyle = ElementTheme.typography.fontBodySmMedium,
)
else -> Box(
modifier = modifier
.semantics {
contentDescription?.let { this.contentDescription = it }
}
.size(size)
.clip(CircleShape)
.background(color)
.then(if (border != null) Modifier.border(border, CircleShape) else Modifier),
)
else -> count?.let {
CounterAtom(
count = count.toInt(),
modifier = modifier
.semantics {
contentDescription?.let { this.contentDescription = it }
}
.then(if (border != null) Modifier.border(border, RoundedCornerShape(percent = 50)) else Modifier),
containerColor = color,
contentColor = ElementTheme.colors.bgCanvasDefault,
textStyle = ElementTheme.typography.fontBodySmMedium,
contentPadding = contentPadding,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun UnreadIndicatorAtomPreview() = ElementPreview {
UnreadIndicatorAtom()
Row(horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
UnreadIndicatorAtom(count = null)
UnreadIndicatorAtom(count = 0)
UnreadIndicatorAtom(count = 1)
UnreadIndicatorAtom(count = 10)
UnreadIndicatorAtom(count = 99)
UnreadIndicatorAtom(count = 999)
}
}
@@ -73,8 +73,6 @@ enum class AvatarSize(val dp: Dp) {
DmCreationConfirmation(64.dp),
UserVerification(52.dp),
OrganizationHeader(64.dp),
SpaceHeader(64.dp),
RoomPreviewHeader(64.dp),
@@ -152,8 +152,10 @@ internal fun remainingPlaybackAnimationMs(
durationMs: Long,
playbackSpeed: Float,
): Int {
val remainingProgress = (1f - progress).coerceAtLeast(0f)
return (remainingProgress * durationMs / playbackSpeed.coerceAtLeast(0.01f))
if (!progress.isFinite() || !playbackSpeed.isFinite() || durationMs <= 0L) return 0
val remainingProgress = (1f - progress).coerceIn(0f, 1f)
val speed = playbackSpeed.coerceAtLeast(0.01f)
return (remainingProgress * durationMs / speed)
.roundToInt()
.coerceAtLeast(0)
}
@@ -109,6 +109,7 @@ fun TextButton(
showProgress: Boolean = false,
destructive: Boolean = false,
leadingIcon: IconSource? = null,
colors: ButtonColors = ButtonStyle.Text.getColors(destructive),
) = ButtonInternal(
text = text,
onClick = onClick,
@@ -118,7 +119,8 @@ fun TextButton(
size = size,
showProgress = showProgress,
destructive = destructive,
leadingIcon = leadingIcon
leadingIcon = leadingIcon,
colors = colors,
)
@Composable
@@ -8,16 +8,17 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingToolbarColors
import androidx.compose.material3.FloatingToolbarDefaults
import androidx.compose.material3.FloatingToolbarHorizontalFabPosition
@@ -27,7 +28,6 @@ import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
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
@@ -137,8 +137,20 @@ fun HorizontalFloatingToolbarItem(
contentColor = ElementTheme.colors.iconSecondary,
)
}
Box {
FilledIconButton(
BadgedBox(
badge = {
counter?.let {
CounterAtom(
count = it,
// Tweak the offset so it's centered on the icon
modifier = Modifier.offset(x = (-16).dp, y = 10.dp),
contentPadding = PaddingValues(vertical = 0.5.dp),
textStyle = ElementTheme.typography.fontBodySmMedium,
)
}
}
) {
IconButton(
modifier = Modifier.widthIn(min = 56.dp),
colors = colors,
onClick = onClick,
@@ -149,15 +161,6 @@ fun HorizontalFloatingToolbarItem(
contentDescription = tooltipLabel,
)
}
if (counter != null) {
CounterAtom(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 6.dp, end = 3.dp),
count = counter,
textStyle = ElementTheme.typography.fontBodyXsMedium,
)
}
}
}
}
@@ -170,18 +173,47 @@ fun HorizontalFloatingToolbarSeparator(modifier: Modifier = Modifier) {
@PreviewsDayNight
@Composable
internal fun HorizontalFloatingToolbarPreview() = ElementPreview {
ContentToPreview(
floatingActionButton = {
FloatingActionButton(
onClick = {},
) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = null,
)
Column {
ContentToPreview(
count = 1,
floatingActionButton = {
FloatingActionButton(
onClick = {},
) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = null,
)
}
}
}
)
)
ContentToPreview(
count = 99,
floatingActionButton = {
FloatingActionButton(
onClick = {},
) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = null,
)
}
}
)
ContentToPreview(
count = 999,
floatingActionButton = {
FloatingActionButton(
onClick = {},
) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = null,
)
}
}
)
}
}
@PreviewsDayNight
@@ -194,10 +226,11 @@ internal fun HorizontalFloatingToolbarNoFabPreview() = ElementPreview {
@Composable
private fun ContentToPreview(
count: Int? = 6,
floatingActionButton: (@Composable () -> Unit)?,
) {
HorizontalFloatingToolbar(
modifier = Modifier.padding(28.dp),
modifier = Modifier.padding(horizontal = 28.dp, vertical = 16.dp),
floatingActionButton = floatingActionButton,
) {
listOf(
@@ -211,7 +244,7 @@ private fun ContentToPreview(
icon = icon,
tooltipLabel = "Label",
isSelected = index == 0,
counter = if (index == 0) 6 else null,
counter = if (index == 0) count else null,
forceRenderingTooltip = true,
onClick = { },
)
@@ -8,12 +8,10 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.BadgedBox
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.atomic.atoms.CounterAtom
@@ -24,16 +22,19 @@ fun NavigationBarIcon(
count: Int = 0,
isCritical: Boolean = false,
) {
Box(modifier) {
BadgedBox(
modifier = modifier,
badge = {
CounterAtom(
textStyle = ElementTheme.typography.fontBodyXsMedium,
count = count,
isCritical = isCritical,
)
}
) {
Icon(
imageVector = imageVector,
contentDescription = null,
)
CounterAtom(
modifier = Modifier.offset(11.dp, (-11).dp),
textStyle = ElementTheme.typography.fontBodyXsMedium,
count = count,
isCritical = isCritical,
)
}
}
@@ -177,4 +177,13 @@ enum class FeatureFlags(
defaultValue = { false },
isFinished = false,
),
ShowAllActivityInRoomList(
key = "feature.show_all_activity_in_room_list",
title = "Show all activity in room list",
description = "Show all activity indicators in the room list, using unread messages instead of notifications.",
defaultValue = { true },
// To hide it from the developer options, since it's displayed in the notification settings instead
isFinished = true,
isInLabs = false,
)
}
@@ -46,10 +46,13 @@ class RustTimelineEventFilterFactory : TimelineEventFilterFactory {
} else {
emptyList()
}
// Exclude any custom state events not supported by the SDK
val customStateEventsFilter = listOf(TimelineEventCondition.AnyCustomStateEvent)
return if (excludedEventTypes.isNotEmpty() || excludedMembershipChanges.isNotEmpty()) {
TimelineEventFilter.Exclude(excludedEventTypes + excludedMembershipChanges)
TimelineEventFilter.Exclude(customStateEventsFilter + excludedEventTypes + excludedMembershipChanges)
} else {
null
TimelineEventFilter.Exclude(customStateEventsFilter)
}
}
}
@@ -245,6 +245,7 @@ class RustSessionVerificationService(
fun destroy() {
Timber.d("Destroying RustSessionVerificationService")
listener = null
verificationStateListenerTaskHandle.cancelAndDestroy()
recoveryStateListenerTaskHandle.cancelAndDestroy()
if (this::verificationController.isInitialized) {
@@ -18,6 +18,7 @@ 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.draw.clipToBounds
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -57,11 +58,13 @@ fun MatrixUserHeader(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
// Name
DisplayNameWithStatus(
name = matrixUser.getBestName(),
status = matrixUser.displayedStatus,
nameColor = ElementTheme.colors.textPrimary,
Text(
modifier = Modifier.clipToBounds(),
text = matrixUser.getBestName(),
maxLines = 1,
style = ElementTheme.typography.fontHeadingMdRegular,
overflow = TextOverflow.Ellipsis,
color = ElementTheme.colors.textPrimary,
)
// Id
if (matrixUser.displayName.isNullOrEmpty().not()) {
@@ -169,7 +169,8 @@ private fun NameAndIndicatorRow(
)
if (showIndicator) {
UnreadIndicatorAtom(
color = ElementTheme.colors.unreadIndicator
color = ElementTheme.colors.unreadIndicator,
count = 0,
)
}
}
@@ -58,19 +58,6 @@ fun AccountInfoCard(
.border(1.dp, ElementTheme.colors.borderDisabled, RoundedCornerShape(8.dp))
.padding(20.dp),
) {
when (state.mode) {
AccountInfoCardMode.Simple -> Unit
is AccountInfoCardMode.DeviceVerification -> Unit
is AccountInfoCardMode.UserVerification -> {
Text(
// TODO
text = "User requesting verification:",
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
Spacer(modifier = Modifier.height(8.dp))
}
}
AccountInfoCardUser(
matrixUser = state.matrixUser,
)
@@ -82,6 +69,12 @@ fun AccountInfoCard(
}
is AccountInfoCardMode.UserVerification -> {
AccountInfoSeparator()
Text(
text = state.mode.hint,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
Spacer(modifier = Modifier.height(8.dp))
AccountInfoCardUser(
matrixUser = state.mode.otherUser,
)
@@ -127,8 +120,7 @@ private fun DeviceInfoRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TextWithLabelMolecule(
// TODO i18n
label = "Signed in",
label = stringResource(CommonStrings.common_signed_in),
text = mode.signInFormattedTimestamp,
modifier = Modifier.weight(1f),
)
@@ -22,9 +22,9 @@ sealed interface AccountInfoCardMode {
data class UserVerification(
val otherUser: MatrixUser,
val hint: String,
) : AccountInfoCardMode
// TODO Replace SessionDetailsView with this component
data class DeviceVerification(
val deviceName: String?,
val deviceId: DeviceId,
@@ -20,6 +20,7 @@ open class AccountInfoCardStatePreviewParam : PreviewParameterProvider<AccountIn
anAccountInfoCardState(
mode = AccountInfoCardMode.UserVerification(
otherUser = aMatrixUser(displayName = "Other User"),
hint = "User request verification hint",
)
),
anAccountInfoCardState(