mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Give the removal confirmation the dialog the team asked for
The dialog now reads "Confirm removal" over "Are you sure you wish to remove this message?", with the reason field marked as optional beneath it, rather than a bare title and a labelled field. TextFieldDialog gained a supporting text so the field can carry that note. The pending removal moves from a nullable timeline item to redactEventAction, an AsyncAction whose Confirming state carries the event id, so it is modelled the same way as every other action the user has to confirm.
This commit is contained in:
+19
-16
@@ -57,6 +57,7 @@ import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
@@ -83,6 +84,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||
import io.element.android.libraries.matrix.ui.model.dmUserStatus
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
@@ -240,7 +242,7 @@ class MessagesPresenter(
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
val eventToRedact = remember { mutableStateOf<TimelineItem.Event?>(null) }
|
||||
val redactEventAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
fun handleEvent(event: MessagesEvent) {
|
||||
when (event) {
|
||||
@@ -252,20 +254,20 @@ class MessagesPresenter(
|
||||
enableTextFormatting = composerState.showTextFormatting,
|
||||
timelineState = timelineState,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
eventToRedact = eventToRedact,
|
||||
redactEventAction = redactEventAction,
|
||||
)
|
||||
}
|
||||
is MessagesEvent.ConfirmRedact -> {
|
||||
val target = eventToRedact.value
|
||||
eventToRedact.value = null
|
||||
if (target != null) {
|
||||
val confirming = redactEventAction.value as? MessagesState.ConfirmingRedaction
|
||||
redactEventAction.value = AsyncAction.Uninitialized
|
||||
if (confirming != null) {
|
||||
localCoroutineScope.launch {
|
||||
redact(target, event.reason?.takeIf { it.isNotBlank() })
|
||||
redact(confirming.eventId.toEventOrTransactionId(), event.reason?.takeIf { it.isNotBlank() })
|
||||
}
|
||||
}
|
||||
}
|
||||
MessagesEvent.CancelRedact -> {
|
||||
eventToRedact.value = null
|
||||
redactEventAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is MessagesEvent.ToggleReaction -> {
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
||||
@@ -345,7 +347,7 @@ class MessagesPresenter(
|
||||
hasUnreadThreads = false,
|
||||
),
|
||||
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom && timelineState.timelineMode !is Timeline.Mode.Thread,
|
||||
eventToRedact = eventToRedact.value,
|
||||
redactEventAction = redactEventAction.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
@@ -384,13 +386,13 @@ class MessagesPresenter(
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
enableTextFormatting: Boolean,
|
||||
timelineState: TimelineState,
|
||||
eventToRedact: MutableState<TimelineItem.Event?>,
|
||||
redactEventAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch {
|
||||
when (action) {
|
||||
TimelineItemAction.CopyText -> handleCopyContents(targetEvent)
|
||||
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
|
||||
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent, eventToRedact)
|
||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent, redactEventAction)
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.EditPoll -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
||||
@@ -512,18 +514,19 @@ class MessagesPresenter(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleActionRedact(event: TimelineItem.Event, eventToRedact: MutableState<TimelineItem.Event?>) {
|
||||
if (event.eventId == null) {
|
||||
private suspend fun handleActionRedact(event: TimelineItem.Event, redactEventAction: MutableState<AsyncAction<Unit>>) {
|
||||
val eventId = event.eventId
|
||||
if (eventId == null) {
|
||||
// The message was never sent, so there is nobody to give a reason to.
|
||||
redact(event, reason = null)
|
||||
redact(event.eventOrTransactionId, reason = null)
|
||||
} else {
|
||||
eventToRedact.value = event
|
||||
redactEventAction.value = MessagesState.ConfirmingRedaction(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun redact(event: TimelineItem.Event, reason: String?) {
|
||||
private suspend fun redact(eventOrTransactionId: EventOrTransactionId, reason: String?) {
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
redactEvent(eventOrTransactionId = event.eventOrTransactionId, reason = reason)
|
||||
redactEvent(eventOrTransactionId = eventOrTransactionId, reason = reason)
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -18,13 +18,14 @@ import io.element.android.features.messages.impl.timeline.TimelineState
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||
@@ -62,8 +63,7 @@ data class MessagesState(
|
||||
val successorRoom: SuccessorRoom?,
|
||||
val threads: Threads,
|
||||
val showLiveLocationShareBanner: Boolean,
|
||||
/** The event the user asked to remove, waiting for them to confirm and optionally give a reason. */
|
||||
val eventToRedact: TimelineItem.Event?,
|
||||
val redactEventAction: AsyncAction<Unit>,
|
||||
val eventSink: (MessagesEvent) -> Unit
|
||||
) {
|
||||
val isTombstoned = successorRoom != null
|
||||
@@ -72,6 +72,11 @@ data class MessagesState(
|
||||
val hasThreads: Boolean,
|
||||
val hasUnreadThreads: Boolean,
|
||||
)
|
||||
|
||||
/** The user asked to remove [eventId] and has to confirm, optionally giving a reason. */
|
||||
data class ConfirmingRedaction(
|
||||
val eventId: EventId,
|
||||
) : AsyncAction.Confirming
|
||||
}
|
||||
|
||||
/** Type of "shared history" icon to show in the top bar. */
|
||||
|
||||
+5
-4
@@ -24,7 +24,6 @@ import io.element.android.features.messages.impl.messagecomposer.aMessageCompose
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent
|
||||
@@ -42,10 +41,12 @@ import io.element.android.features.roomcall.api.aStandByCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
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.ROOM_NAME
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
@@ -93,7 +94,7 @@ open class MessagesStatePreviewParam : PreviewParameterProvider<MessagesState> {
|
||||
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
|
||||
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange()))
|
||||
),
|
||||
aMessagesState(eventToRedact = aTimelineItemEvent()),
|
||||
aMessagesState(redactEventAction = MessagesState.ConfirmingRedaction(EventId("\$anEventId"))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,7 +134,7 @@ fun aMessagesState(
|
||||
isCurrentlySharingLiveLocationInRoom: Boolean = false,
|
||||
dmUserStatus: DisplayedStatus? = null,
|
||||
eventSink: (MessagesEvent) -> Unit = {},
|
||||
eventToRedact: TimelineItem.Event? = null,
|
||||
redactEventAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
) = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
roomName = roomName,
|
||||
@@ -164,7 +165,7 @@ fun aMessagesState(
|
||||
threads = threads,
|
||||
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
|
||||
dmUserStatus = dmUserStatus,
|
||||
eventToRedact = eventToRedact,
|
||||
redactEventAction = redactEventAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
||||
+5
-4
@@ -457,11 +457,12 @@ fun MessagesView(
|
||||
},
|
||||
)
|
||||
|
||||
if (state.eventToRedact != null) {
|
||||
if (state.redactEventAction is MessagesState.ConfirmingRedaction) {
|
||||
TextFieldDialog(
|
||||
title = stringResource(CommonStrings.action_remove_message),
|
||||
label = stringResource(CommonStrings.common_reason),
|
||||
placeholder = null,
|
||||
title = stringResource(CommonStrings.screen_room_confirm_removal_title),
|
||||
content = stringResource(CommonStrings.screen_room_confirm_removal_message),
|
||||
placeholder = stringResource(CommonStrings.common_reason),
|
||||
supportingText = stringResource(CommonStrings.common_optional),
|
||||
value = null,
|
||||
submitText = stringResource(CommonStrings.action_remove),
|
||||
destructiveSubmit = true,
|
||||
|
||||
+6
-5
@@ -45,6 +45,7 @@ import io.element.android.features.messages.test.timeline.voicemessages.composer
|
||||
import io.element.android.features.roomcall.api.aStandByCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
@@ -561,11 +562,11 @@ class MessagesPresenterTest {
|
||||
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, messageEvent))
|
||||
advanceUntilIdle()
|
||||
val confirmingState = expectMostRecentItem()
|
||||
assertThat(confirmingState.eventToRedact).isEqualTo(messageEvent)
|
||||
assertThat(confirmingState.redactEventAction).isEqualTo(MessagesState.ConfirmingRedaction(messageEvent.eventId!!))
|
||||
assert(redactEventLambda).isNeverCalled()
|
||||
confirmingState.eventSink(MessagesEvent.ConfirmRedact(A_REASON))
|
||||
advanceUntilIdle()
|
||||
assertThat(expectMostRecentItem().eventToRedact).isNull()
|
||||
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assert(redactEventLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(messageEvent.eventOrTransactionId), value(A_REASON))
|
||||
@@ -589,7 +590,7 @@ class MessagesPresenterTest {
|
||||
advanceUntilIdle()
|
||||
expectMostRecentItem().eventSink(MessagesEvent.ConfirmRedact(" "))
|
||||
advanceUntilIdle()
|
||||
assertThat(expectMostRecentItem().eventToRedact).isNull()
|
||||
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assert(redactEventLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(messageEvent.eventOrTransactionId), value(null))
|
||||
@@ -612,7 +613,7 @@ class MessagesPresenterTest {
|
||||
advanceUntilIdle()
|
||||
expectMostRecentItem().eventSink(MessagesEvent.CancelRedact)
|
||||
advanceUntilIdle()
|
||||
assertThat(expectMostRecentItem().eventToRedact).isNull()
|
||||
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assert(redactEventLambda).isNeverCalled()
|
||||
}
|
||||
}
|
||||
@@ -632,7 +633,7 @@ class MessagesPresenterTest {
|
||||
val localEcho = aMessageEvent(eventId = null, transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, localEcho))
|
||||
advanceUntilIdle()
|
||||
assertThat(expectMostRecentItem().eventToRedact).isNull()
|
||||
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assert(redactEventLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(localEcho.eventOrTransactionId), value(null))
|
||||
|
||||
+2
@@ -47,6 +47,7 @@ fun TextFieldDialog(
|
||||
maxLines: Int = minLines,
|
||||
content: String? = null,
|
||||
label: String? = null,
|
||||
supportingText: String? = null,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
submitText: String = stringResource(CommonStrings.action_ok),
|
||||
destructiveSubmit: Boolean = false,
|
||||
@@ -84,6 +85,7 @@ fun TextFieldDialog(
|
||||
TextFieldListItem(
|
||||
placeholder = placeholder.orEmpty(),
|
||||
label = label,
|
||||
supportingText = supportingText,
|
||||
text = textFieldContents,
|
||||
onTextChange = {
|
||||
error = if (!validation(it.text)) onValidationErrorMessage else null
|
||||
|
||||
+2
-1
@@ -57,6 +57,7 @@ fun TextFieldListItem(
|
||||
minLines: Int = 1,
|
||||
maxLines: Int = minLines,
|
||||
label: String? = null,
|
||||
supportingText: String? = null,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
@@ -66,7 +67,7 @@ fun TextFieldListItem(
|
||||
placeholder = placeholder,
|
||||
label = label,
|
||||
validity = if (error != null) TextFieldValidity.Invalid else TextFieldValidity.None,
|
||||
supportingText = error,
|
||||
supportingText = error ?: supportingText,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
maxLines = maxLines,
|
||||
|
||||
@@ -10,4 +10,7 @@
|
||||
<string name="a11y_state_collapsed">"Collapsed"</string>
|
||||
<string name="a11y_active">Active</string>
|
||||
<string name="a11y_inactive">Inactive</string>
|
||||
<string name="common_optional">"(Optional)"</string>
|
||||
<string name="screen_room_confirm_removal_title">"Confirm removal"</string>
|
||||
<string name="screen_room_confirm_removal_message">"Are you sure you wish to remove this message?"</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user