mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 01:19:33 +00:00
Merge pull request #7538 from hayaksi1/fix/1612-redaction-reason
Let the user give a reason when removing a message
This commit is contained in:
+2
@@ -18,6 +18,8 @@ sealed interface MessagesEvent {
|
|||||||
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvent
|
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvent
|
||||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvent
|
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvent
|
||||||
data class OnUserClicked(val user: MatrixUser) : MessagesEvent
|
data class OnUserClicked(val user: MatrixUser) : MessagesEvent
|
||||||
|
data class ConfirmRedact(val reason: String?) : MessagesEvent
|
||||||
|
data object CancelRedact : MessagesEvent
|
||||||
data object StopLiveLocationShare : MessagesEvent
|
data object StopLiveLocationShare : MessagesEvent
|
||||||
data object ShowLiveLocationShare : MessagesEvent
|
data object ShowLiveLocationShare : MessagesEvent
|
||||||
data object MarkAsFullyReadAndExit : MessagesEvent
|
data object MarkAsFullyReadAndExit : MessagesEvent
|
||||||
|
|||||||
+32
-3
@@ -58,6 +58,7 @@ import io.element.android.features.roomcall.api.RoomCallState
|
|||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
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.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
@@ -84,6 +85,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.room.powerlevels.permissionsAsState
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
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.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.messages.reply.map
|
||||||
import io.element.android.libraries.matrix.ui.model.dmUserStatus
|
import io.element.android.libraries.matrix.ui.model.dmUserStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
@@ -241,6 +243,8 @@ class MessagesPresenter(
|
|||||||
onPauseOrDispose {}
|
onPauseOrDispose {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val redactEventAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
fun handleEvent(event: MessagesEvent) {
|
fun handleEvent(event: MessagesEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is MessagesEvent.HandleAction -> {
|
is MessagesEvent.HandleAction -> {
|
||||||
@@ -251,8 +255,21 @@ class MessagesPresenter(
|
|||||||
enableTextFormatting = composerState.showTextFormatting,
|
enableTextFormatting = composerState.showTextFormatting,
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
timelineProtectionState = timelineProtectionState,
|
timelineProtectionState = timelineProtectionState,
|
||||||
|
redactEventAction = redactEventAction,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is MessagesEvent.ConfirmRedact -> {
|
||||||
|
val confirming = redactEventAction.value as? MessagesState.ConfirmingRedaction
|
||||||
|
redactEventAction.value = AsyncAction.Uninitialized
|
||||||
|
if (confirming != null) {
|
||||||
|
localCoroutineScope.launch {
|
||||||
|
redact(confirming.eventId.toEventOrTransactionId(), event.reason?.takeIf { it.isNotBlank() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MessagesEvent.CancelRedact -> {
|
||||||
|
redactEventAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
is MessagesEvent.ToggleReaction -> {
|
is MessagesEvent.ToggleReaction -> {
|
||||||
localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
||||||
}
|
}
|
||||||
@@ -331,6 +348,7 @@ class MessagesPresenter(
|
|||||||
hasUnreadThreads = false,
|
hasUnreadThreads = false,
|
||||||
),
|
),
|
||||||
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom && timelineState.timelineMode !is Timeline.Mode.Thread,
|
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom && timelineState.timelineMode !is Timeline.Mode.Thread,
|
||||||
|
redactEventAction = redactEventAction.value,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -369,12 +387,13 @@ class MessagesPresenter(
|
|||||||
timelineProtectionState: TimelineProtectionState,
|
timelineProtectionState: TimelineProtectionState,
|
||||||
enableTextFormatting: Boolean,
|
enableTextFormatting: Boolean,
|
||||||
timelineState: TimelineState,
|
timelineState: TimelineState,
|
||||||
|
redactEventAction: MutableState<AsyncAction<Unit>>,
|
||||||
) = launch {
|
) = launch {
|
||||||
when (action) {
|
when (action) {
|
||||||
TimelineItemAction.CopyText -> handleCopyContents(targetEvent)
|
TimelineItemAction.CopyText -> handleCopyContents(targetEvent)
|
||||||
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
|
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
|
||||||
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
||||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
TimelineItemAction.Redact -> handleActionRedact(targetEvent, redactEventAction)
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.EditPoll -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
TimelineItemAction.EditPoll -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||||
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
||||||
@@ -496,9 +515,19 @@ class MessagesPresenter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
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.eventOrTransactionId, reason = null)
|
||||||
|
} else {
|
||||||
|
redactEventAction.value = MessagesState.ConfirmingRedaction(eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun redact(eventOrTransactionId: EventOrTransactionId, reason: String?) {
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
redactEvent(eventOrTransactionId = event.eventOrTransactionId, reason = null)
|
redactEvent(eventOrTransactionId = eventOrTransactionId, reason = reason)
|
||||||
.onFailure { Timber.e(it) }
|
.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -21,9 +21,11 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot
|
|||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||||
import io.element.android.features.roomcall.api.RoomCallState
|
import io.element.android.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
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.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||||
@@ -61,6 +63,7 @@ data class MessagesState(
|
|||||||
val successorRoom: SuccessorRoom?,
|
val successorRoom: SuccessorRoom?,
|
||||||
val threads: Threads,
|
val threads: Threads,
|
||||||
val showLiveLocationShareBanner: Boolean,
|
val showLiveLocationShareBanner: Boolean,
|
||||||
|
val redactEventAction: AsyncAction<Unit>,
|
||||||
val eventSink: (MessagesEvent) -> Unit
|
val eventSink: (MessagesEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val isTombstoned = successorRoom != null
|
val isTombstoned = successorRoom != null
|
||||||
@@ -69,6 +72,11 @@ data class MessagesState(
|
|||||||
val hasThreads: Boolean,
|
val hasThreads: Boolean,
|
||||||
val hasUnreadThreads: 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. */
|
/** Type of "shared history" icon to show in the top bar. */
|
||||||
|
|||||||
+5
@@ -41,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.RoomMemberModerationEvent
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
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.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ROOM_NAME
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
@@ -92,6 +94,7 @@ open class MessagesStatePreviewParam : PreviewParameterProvider<MessagesState> {
|
|||||||
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
|
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
|
||||||
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange()))
|
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange()))
|
||||||
),
|
),
|
||||||
|
aMessagesState(redactEventAction = MessagesState.ConfirmingRedaction(EventId("\$anEventId"))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +134,7 @@ fun aMessagesState(
|
|||||||
isCurrentlySharingLiveLocationInRoom: Boolean = false,
|
isCurrentlySharingLiveLocationInRoom: Boolean = false,
|
||||||
dmUserStatus: DisplayedStatus? = null,
|
dmUserStatus: DisplayedStatus? = null,
|
||||||
eventSink: (MessagesEvent) -> Unit = {},
|
eventSink: (MessagesEvent) -> Unit = {},
|
||||||
|
redactEventAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
) = MessagesState(
|
) = MessagesState(
|
||||||
roomId = RoomId("!id:domain"),
|
roomId = RoomId("!id:domain"),
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
@@ -161,6 +165,7 @@ fun aMessagesState(
|
|||||||
threads = threads,
|
threads = threads,
|
||||||
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
|
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
|
||||||
dmUserStatus = dmUserStatus,
|
dmUserStatus = dmUserStatus,
|
||||||
|
redactEventAction = redactEventAction,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+15
@@ -104,6 +104,7 @@ import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertM
|
|||||||
import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayout
|
import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayout
|
||||||
import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayoutState
|
import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayoutState
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.TextFieldDialog
|
||||||
import io.element.android.libraries.designsystem.components.rememberExpandableBottomSheetLayoutState
|
import io.element.android.libraries.designsystem.components.rememberExpandableBottomSheetLayoutState
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
@@ -455,6 +456,20 @@ fun MessagesView(
|
|||||||
state.timelineState.eventSink(TimelineEvent.HideSendFailureDialog)
|
state.timelineState.eventSink(TimelineEvent.HideSendFailureDialog)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (state.redactEventAction is MessagesState.ConfirmingRedaction) {
|
||||||
|
TextFieldDialog(
|
||||||
|
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,
|
||||||
|
onSubmit = { reason -> state.eventSink(MessagesEvent.ConfirmRedact(reason)) },
|
||||||
|
onDismissRequest = { state.eventSink(MessagesEvent.CancelRedact) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+77
-1
@@ -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.roomcall.api.aStandByCallState
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
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.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
@@ -78,10 +79,12 @@ import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
|||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_CAPTION
|
import io.element.android.libraries.matrix.test.A_CAPTION
|
||||||
|
import io.element.android.libraries.matrix.test.A_REASON
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.core.FakeSendHandle
|
import io.element.android.libraries.matrix.test.core.FakeSendHandle
|
||||||
@@ -557,13 +560,86 @@ class MessagesPresenterTest {
|
|||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val messageEvent = aMessageEvent()
|
val messageEvent = aMessageEvent()
|
||||||
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, messageEvent))
|
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, messageEvent))
|
||||||
awaitItem()
|
advanceUntilIdle()
|
||||||
|
val confirmingState = expectMostRecentItem()
|
||||||
|
assertThat(confirmingState.redactEventAction).isEqualTo(MessagesState.ConfirmingRedaction(messageEvent.eventId!!))
|
||||||
|
assert(redactEventLambda).isNeverCalled()
|
||||||
|
confirmingState.eventSink(MessagesEvent.ConfirmRedact(A_REASON))
|
||||||
|
advanceUntilIdle()
|
||||||
|
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
assert(redactEventLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(messageEvent.eventOrTransactionId), value(A_REASON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action redact - a blank reason is not sent`() = runTest {
|
||||||
|
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
|
val liveTimeline = FakeTimeline()
|
||||||
|
val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) }
|
||||||
|
liveTimeline.redactEventLambda = redactEventLambda
|
||||||
|
val presenter = createMessagesPresenter(
|
||||||
|
timeline = liveTimeline,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
presenter.testWithLifecycleOwner {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent()
|
||||||
|
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, messageEvent))
|
||||||
|
advanceUntilIdle()
|
||||||
|
expectMostRecentItem().eventSink(MessagesEvent.ConfirmRedact(" "))
|
||||||
|
advanceUntilIdle()
|
||||||
|
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
assert(redactEventLambda)
|
assert(redactEventLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(messageEvent.eventOrTransactionId), value(null))
|
.with(value(messageEvent.eventOrTransactionId), value(null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action redact - cancelling does not redact`() = runTest {
|
||||||
|
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
|
val liveTimeline = FakeTimeline()
|
||||||
|
val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) }
|
||||||
|
liveTimeline.redactEventLambda = redactEventLambda
|
||||||
|
val presenter = createMessagesPresenter(
|
||||||
|
timeline = liveTimeline,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
presenter.testWithLifecycleOwner {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
|
||||||
|
advanceUntilIdle()
|
||||||
|
expectMostRecentItem().eventSink(MessagesEvent.CancelRedact)
|
||||||
|
advanceUntilIdle()
|
||||||
|
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
assert(redactEventLambda).isNeverCalled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action redact - a message which was never sent is redacted without asking`() = runTest {
|
||||||
|
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||||
|
val liveTimeline = FakeTimeline()
|
||||||
|
val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) }
|
||||||
|
liveTimeline.redactEventLambda = redactEventLambda
|
||||||
|
val presenter = createMessagesPresenter(
|
||||||
|
timeline = liveTimeline,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
presenter.testWithLifecycleOwner {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val localEcho = aMessageEvent(eventId = null, transactionId = A_TRANSACTION_ID)
|
||||||
|
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, localEcho))
|
||||||
|
advanceUntilIdle()
|
||||||
|
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
assert(redactEventLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(localEcho.eventOrTransactionId), value(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action report content`() = runTest {
|
fun `present - handle action report content`() = runTest {
|
||||||
val onReportContentClickLambda = lambdaRecorder { _: EventId, _: UserId -> }
|
val onReportContentClickLambda = lambdaRecorder { _: EventId, _: UserId -> }
|
||||||
|
|||||||
+2
@@ -47,6 +47,7 @@ fun TextFieldDialog(
|
|||||||
maxLines: Int = minLines,
|
maxLines: Int = minLines,
|
||||||
content: String? = null,
|
content: String? = null,
|
||||||
label: String? = null,
|
label: String? = null,
|
||||||
|
supportingText: String? = null,
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
submitText: String = stringResource(CommonStrings.action_ok),
|
submitText: String = stringResource(CommonStrings.action_ok),
|
||||||
destructiveSubmit: Boolean = false,
|
destructiveSubmit: Boolean = false,
|
||||||
@@ -84,6 +85,7 @@ fun TextFieldDialog(
|
|||||||
TextFieldListItem(
|
TextFieldListItem(
|
||||||
placeholder = placeholder.orEmpty(),
|
placeholder = placeholder.orEmpty(),
|
||||||
label = label,
|
label = label,
|
||||||
|
supportingText = supportingText,
|
||||||
text = textFieldContents,
|
text = textFieldContents,
|
||||||
onTextChange = {
|
onTextChange = {
|
||||||
error = if (!validation(it.text)) onValidationErrorMessage else null
|
error = if (!validation(it.text)) onValidationErrorMessage else null
|
||||||
|
|||||||
+2
-1
@@ -57,6 +57,7 @@ fun TextFieldListItem(
|
|||||||
minLines: Int = 1,
|
minLines: Int = 1,
|
||||||
maxLines: Int = minLines,
|
maxLines: Int = minLines,
|
||||||
label: String? = null,
|
label: String? = null,
|
||||||
|
supportingText: String? = null,
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
) {
|
) {
|
||||||
@@ -66,7 +67,7 @@ fun TextFieldListItem(
|
|||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
label = label,
|
label = label,
|
||||||
validity = if (error != null) TextFieldValidity.Invalid else TextFieldValidity.None,
|
validity = if (error != null) TextFieldValidity.Invalid else TextFieldValidity.None,
|
||||||
supportingText = error,
|
supportingText = error ?: supportingText,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
|
|||||||
@@ -10,4 +10,7 @@
|
|||||||
<string name="a11y_state_collapsed">"Collapsed"</string>
|
<string name="a11y_state_collapsed">"Collapsed"</string>
|
||||||
<string name="a11y_active">Active</string>
|
<string name="a11y_active">Active</string>
|
||||||
<string name="a11y_inactive">Inactive</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>
|
</resources>
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user