Merge pull request #3461 from element-hq/feature/fga/send_failure_identity_changes
Require acknowledgement to send to a verified user if their identity changed or if a device is unverified.
This commit is contained in:
commit
60365e46a5
50 changed files with 1355 additions and 87 deletions
|
|
@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
|||
* @param resourceId the resource id of the icon to display, exclusive with [imageVector]
|
||||
* @param imageVector the image vector of the icon to display, exclusive with [resourceId]
|
||||
* @param tint the tint to apply to the icon
|
||||
* @param backgroundTint the tint to apply to the icon background
|
||||
*/
|
||||
@Composable
|
||||
fun RoundedIconAtom(
|
||||
|
|
@ -44,13 +45,14 @@ fun RoundedIconAtom(
|
|||
size: RoundedIconAtomSize = RoundedIconAtomSize.Large,
|
||||
resourceId: Int? = null,
|
||||
imageVector: ImageVector? = null,
|
||||
tint: Color = MaterialTheme.colorScheme.secondary
|
||||
tint: Color = MaterialTheme.colorScheme.secondary,
|
||||
backgroundTint: Color = ElementTheme.colors.temporaryColorBgSpecial,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size.toContainerSize())
|
||||
.background(
|
||||
color = ElementTheme.colors.temporaryColorBgSpecial,
|
||||
color = backgroundTint,
|
||||
shape = RoundedCornerShape(size.toCornerSize())
|
||||
)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
||||
|
||||
/**
|
||||
* IconTitleSubtitleMolecule is a molecule which displays an icon, a title and a subtitle.
|
||||
|
|
@ -37,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
* @param iconResourceId the resource id of the icon to display, exclusive with [iconImageVector]
|
||||
* @param iconImageVector the image vector of the icon to display, exclusive with [iconResourceId]
|
||||
* @param iconTint the tint to apply to the icon
|
||||
* @param iconBackgroundTint the tint to apply to the icon background
|
||||
*/
|
||||
@Composable
|
||||
fun IconTitleSubtitleMolecule(
|
||||
|
|
@ -46,6 +48,7 @@ fun IconTitleSubtitleMolecule(
|
|||
iconResourceId: Int? = null,
|
||||
iconImageVector: ImageVector? = null,
|
||||
iconTint: Color = MaterialTheme.colorScheme.primary,
|
||||
iconBackgroundTint: Color = ElementTheme.colors.temporaryColorBgSpecial,
|
||||
) {
|
||||
Column(modifier) {
|
||||
RoundedIconAtom(
|
||||
|
|
@ -55,6 +58,7 @@ fun IconTitleSubtitleMolecule(
|
|||
resourceId = iconResourceId,
|
||||
imageVector = iconImageVector,
|
||||
tint = iconTint,
|
||||
backgroundTint = iconBackgroundTint,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -65,7 +65,41 @@ fun ListItem(
|
|||
disabledLeadingIconColor = ListItemDefaultColors.iconDisabled,
|
||||
disabledTrailingIconColor = ListItemDefaultColors.iconDisabled,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = headlineContent,
|
||||
modifier = modifier,
|
||||
supportingContent = supportingContent,
|
||||
leadingContent = leadingContent,
|
||||
trailingContent = trailingContent,
|
||||
colors = colors,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A List Item component to be used in lists and menus with simple layouts, matching the Material 3 guidelines.
|
||||
* @param headlineContent The main content of the list item, usually a text.
|
||||
* @param colors The colors to use for the list item. You can use [ListItemDefaults.colors] to create this.
|
||||
* @param modifier The modifier to be applied to the list item.
|
||||
* @param supportingContent The content to be displayed below the headline content.
|
||||
* @param leadingContent The content to be displayed before the headline content.
|
||||
* @param trailingContent The content to be displayed after the headline content.
|
||||
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
|
||||
* @param onClick The callback to be called when the list item is clicked.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun ListItem(
|
||||
headlineContent: @Composable () -> Unit,
|
||||
colors: ListItemColors,
|
||||
modifier: Modifier = Modifier,
|
||||
supportingContent: @Composable (() -> Unit)? = null,
|
||||
leadingContent: ListItemContent? = null,
|
||||
trailingContent: ListItemContent? = null,
|
||||
enabled: Boolean = true,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
// We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
|
||||
val headlineColor = if (enabled) colors.headlineColor else colors.disabledHeadlineColor
|
||||
val leadingContentColor = if (enabled) colors.leadingIconColor else colors.disabledLeadingIconColor
|
||||
|
|
@ -378,8 +412,8 @@ private object PreviewItems {
|
|||
) {
|
||||
ElementThemedPreview {
|
||||
ListItem(
|
||||
headlineContent = PreviewItems.headline(),
|
||||
supportingContent = PreviewItems.text(),
|
||||
headlineContent = headline(),
|
||||
supportingContent = text(),
|
||||
leadingContent = leadingContent,
|
||||
trailingContent = trailingContent,
|
||||
style = style,
|
||||
|
|
@ -397,8 +431,8 @@ private object PreviewItems {
|
|||
) {
|
||||
ElementThemedPreview {
|
||||
ListItem(
|
||||
headlineContent = PreviewItems.headline(),
|
||||
supportingContent = PreviewItems.textSingleLine(),
|
||||
headlineContent = headline(),
|
||||
supportingContent = textSingleLine(),
|
||||
leadingContent = leadingContent,
|
||||
trailingContent = trailingContent,
|
||||
style = style,
|
||||
|
|
@ -417,7 +451,7 @@ private object PreviewItems {
|
|||
) {
|
||||
ElementThemedPreview {
|
||||
ListItem(
|
||||
headlineContent = PreviewItems.headline(),
|
||||
headlineContent = headline(),
|
||||
leadingContent = leadingContent,
|
||||
trailingContent = trailingContent,
|
||||
enabled = enabled,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
|
|
@ -349,5 +350,24 @@ interface MatrixRoom : Closeable {
|
|||
*/
|
||||
suspend fun clearComposerDraft(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Ignore the local trust for the given devices and resend messages that failed to send because said devices are unverified.
|
||||
*
|
||||
* @param devices The map of users identifiers to device identifiers received in the error
|
||||
* @param transactionId The send queue transaction identifier of the local echo the send error applies to.
|
||||
*
|
||||
*/
|
||||
suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, transactionId: TransactionId): Result<Unit>
|
||||
|
||||
/**
|
||||
* Remove verification requirements for the given users and
|
||||
* resend messages that failed to send because their identities were no longer verified.
|
||||
*
|
||||
* @param userIds The list of users identifiers received in the error.
|
||||
* @param transactionId The send queue transaction identifier of the local echo the send error applies to.
|
||||
*
|
||||
*/
|
||||
suspend fun withdrawVerificationAndResend(userIds: List<UserId>, transactionId: TransactionId): Result<Unit>
|
||||
|
||||
override fun close() = destroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.libraries.matrix.api.timeline.item.event
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
|
|
@ -18,21 +19,24 @@ sealed interface LocalEventSendState {
|
|||
data class Unknown(val error: String) : Failed
|
||||
data object CrossSigningNotSetup : Failed
|
||||
data object SendingFromUnverifiedDevice : Failed
|
||||
|
||||
sealed interface VerifiedUser : Failed
|
||||
data class VerifiedUserHasUnsignedDevice(
|
||||
/**
|
||||
* The unsigned devices belonging to verified users. A map from user ID
|
||||
* to a list of device IDs.
|
||||
*/
|
||||
val devices: Map<UserId, List<String>>
|
||||
) : Failed
|
||||
val devices: Map<UserId, List<DeviceId>>
|
||||
) : VerifiedUser
|
||||
|
||||
data class VerifiedUserChangedIdentity(
|
||||
/**
|
||||
* The users that were previously verified but are no longer.
|
||||
*/
|
||||
val users: List<UserId>
|
||||
) : Failed
|
||||
) : VerifiedUser
|
||||
}
|
||||
|
||||
data class Sent(
|
||||
val eventId: EventId
|
||||
) : LocalEventSendState
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
strategy = if (featureFlagService.isFeatureEnabled(FeatureFlags.InvisibleCrypto)) {
|
||||
CollectStrategy.IdentityBasedStrategy
|
||||
} else {
|
||||
CollectStrategy.DeviceBasedStrategy(onlyAllowTrustedDevices = false, errorOnVerifiedUserProblem = false)
|
||||
CollectStrategy.DeviceBasedStrategy(onlyAllowTrustedDevices = false, errorOnVerifiedUserProblem = true)
|
||||
}
|
||||
)
|
||||
.run {
|
||||
|
|
|
|||
|
|
@ -459,8 +459,8 @@ class RustMatrixRoom(
|
|||
return liveTimeline.forwardEvent(eventId, roomIds)
|
||||
}
|
||||
|
||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> {
|
||||
return Result.failure(UnsupportedOperationException("Not supported"))
|
||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = runCatching {
|
||||
innerRoom.tryResend(transactionId.value)
|
||||
}
|
||||
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> {
|
||||
|
|
@ -645,6 +645,22 @@ class RustMatrixRoom(
|
|||
innerRoom.clearComposerDraft()
|
||||
}
|
||||
|
||||
override suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, transactionId: TransactionId) = runCatching {
|
||||
innerRoom.ignoreDeviceTrustAndResend(
|
||||
devices = devices.entries.associate { entry ->
|
||||
entry.key.value to entry.value.map { it.value }
|
||||
},
|
||||
transactionId = transactionId.value
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun withdrawVerificationAndResend(userIds: List<UserId>, transactionId: TransactionId) = runCatching {
|
||||
innerRoom.withdrawVerificationAndResend(
|
||||
userIds = userIds.map { it.value },
|
||||
transactionId = transactionId.value
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTimeline(
|
||||
timeline: InnerTimeline,
|
||||
mode: Timeline.Mode,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.timeline.item.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -87,7 +88,9 @@ fun RustEventSendState?.map(): LocalEventSendState? {
|
|||
}
|
||||
is RustEventSendState.VerifiedUserHasUnsignedDevice -> {
|
||||
LocalEventSendState.Failed.VerifiedUserHasUnsignedDevice(
|
||||
devices = devices.mapKeys { UserId(it.key) }
|
||||
devices = devices.entries.associate { entry ->
|
||||
UserId(entry.key) to entry.value.map { DeviceId(it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
EventSendState.CrossSigningNotSetup -> LocalEventSendState.Failed.CrossSigningNotSetup
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.test.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
|
|
@ -79,7 +80,7 @@ class FakeMatrixRoom(
|
|||
private var roomPermalinkResult: () -> Result<String> = { lambdaError() },
|
||||
private var eventPermalinkResult: (EventId) -> Result<String> = { lambdaError() },
|
||||
private val sendCallNotificationIfNeededResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val userDisplayNameResult: () -> Result<String?> = { lambdaError() },
|
||||
private val userDisplayNameResult: (UserId) -> Result<String?> = { lambdaError() },
|
||||
private val userAvatarUrlResult: () -> Result<String?> = { lambdaError() },
|
||||
private val userRoleResult: () -> Result<RoomMember.Role> = { lambdaError() },
|
||||
private val getUpdatedMemberResult: (UserId) -> Result<RoomMember> = { lambdaError() },
|
||||
|
|
@ -134,7 +135,9 @@ class FakeMatrixRoom(
|
|||
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
|
||||
private val clearComposerDraftLambda: () -> Result<Unit> = { Result.success(Unit) },
|
||||
private val subscribeToSyncLambda: () -> Unit = { lambdaError() },
|
||||
) : MatrixRoom {
|
||||
private val ignoreDeviceTrustAndResendResult: (Map<UserId, List<DeviceId>>, TransactionId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val withdrawVerificationAndResendResult: (List<UserId>, TransactionId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
) : MatrixRoom {
|
||||
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1)
|
||||
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
|
||||
|
||||
|
|
@ -199,7 +202,7 @@ class FakeMatrixRoom(
|
|||
override fun destroy() = Unit
|
||||
|
||||
override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask {
|
||||
userDisplayNameResult()
|
||||
userDisplayNameResult(userId)
|
||||
}
|
||||
|
||||
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = simulateLongTask {
|
||||
|
|
@ -226,7 +229,7 @@ class FakeMatrixRoom(
|
|||
return toggleReactionResult(emoji, uniqueId)
|
||||
}
|
||||
|
||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> {
|
||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = simulateLongTask {
|
||||
return retrySendMessageResult(transactionId)
|
||||
}
|
||||
|
||||
|
|
@ -492,6 +495,14 @@ class FakeMatrixRoom(
|
|||
return getWidgetDriverResult(widgetSettings)
|
||||
}
|
||||
|
||||
override suspend fun ignoreDeviceTrustAndResend(devices: Map<UserId, List<DeviceId>>, transactionId: TransactionId): Result<Unit> = simulateLongTask {
|
||||
return ignoreDeviceTrustAndResendResult(devices, transactionId)
|
||||
}
|
||||
|
||||
override suspend fun withdrawVerificationAndResend(userIds: List<UserId>, transactionId: TransactionId): Result<Unit> = simulateLongTask {
|
||||
return withdrawVerificationAndResendResult(userIds, transactionId)
|
||||
}
|
||||
|
||||
fun givenRoomMembersState(state: MatrixRoomMembersState) {
|
||||
membersStateFlow.value = state
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue