[Message Actions] Retry sending failed messages (#596)
* Add `RetrySendMessageMenu` to retry sending failed messages or removing its local echo. * Fix initial event being retrieved, not the updated one --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
7aca5c904c
commit
31555c4bd0
41 changed files with 641 additions and 37 deletions
1
changelog.d/487.feature
Normal file
1
changelog.d/487.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Add menu to retry sending failed messages or delete their local echoes.
|
||||
|
|
@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
|||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
|
|
@ -65,6 +66,7 @@ class MessagesPresenter @Inject constructor(
|
|||
private val composerPresenter: MessageComposerPresenter,
|
||||
private val timelinePresenter: TimelinePresenter,
|
||||
private val actionListPresenter: ActionListPresenter,
|
||||
private val retrySendMenuPresenter: RetrySendMenuPresenter,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||
|
|
@ -77,6 +79,7 @@ class MessagesPresenter @Inject constructor(
|
|||
val composerState = composerPresenter.present()
|
||||
val timelineState = timelinePresenter.present()
|
||||
val actionListState = actionListPresenter.present()
|
||||
val retryState = retrySendMenuPresenter.present()
|
||||
|
||||
val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L)
|
||||
val roomName: MutableState<String?> = rememberSaveable {
|
||||
|
|
@ -116,6 +119,7 @@ class MessagesPresenter @Inject constructor(
|
|||
composerState = composerState,
|
||||
timelineState = timelineState,
|
||||
actionListState = actionListState,
|
||||
retrySendMenuState = retryState,
|
||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
snackbarMessage = snackbarMessage,
|
||||
eventSink = ::handleEvents
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable
|
|||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -32,6 +33,7 @@ data class MessagesState(
|
|||
val composerState: MessageComposerState,
|
||||
val timelineState: TimelineState,
|
||||
val actionListState: ActionListState,
|
||||
val retrySendMenuState: RetrySendMenuState,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val eventSink: (MessagesEvents) -> Unit
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState
|
|||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||
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.retrysendmenu.RetrySendMenuState
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -48,6 +49,10 @@ fun aMessagesState() = MessagesState(
|
|||
timelineState = aTimelineState().copy(
|
||||
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
|
||||
),
|
||||
retrySendMenuState = RetrySendMenuState(
|
||||
selectedEvent = null,
|
||||
eventSink = {},
|
||||
),
|
||||
actionListState = anActionListState(),
|
||||
hasNetworkConnection = true,
|
||||
snackbarMessage = null,
|
||||
|
|
|
|||
|
|
@ -34,14 +34,10 @@ import androidx.compose.foundation.layout.wrapContentHeight
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -59,7 +55,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListView
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
|
|
@ -67,6 +62,8 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat
|
|||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||
import io.element.android.features.messages.impl.timeline.components.CustomReactionBottomSheet
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||
|
|
@ -85,6 +82,7 @@ import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
|
@ -176,6 +174,11 @@ fun MessagesView(
|
|||
onMessageClicked = ::onMessageClicked,
|
||||
onMessageLongClicked = ::onMessageLongClicked,
|
||||
onUserDataClicked = onUserDataClicked,
|
||||
onTimestampClicked = { event ->
|
||||
if (event.sendState is EventSendState.SendingFailed) {
|
||||
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
|
|
@ -225,6 +228,10 @@ fun MessagesView(
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
RetrySendMessageMenu(
|
||||
state = state.retrySendMenuState
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -244,10 +251,11 @@ private fun AttachmentStateView(
|
|||
@Composable
|
||||
fun MessagesViewContent(
|
||||
state: MessagesState,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit,
|
||||
onUserDataClicked: (UserId) -> Unit,
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onUserDataClicked: (UserId) -> Unit = {},
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -263,6 +271,7 @@ fun MessagesViewContent(
|
|||
onMessageClicked = onMessageClicked,
|
||||
onMessageLongClicked = onMessageLongClicked,
|
||||
onUserDataClicked = onUserDataClicked,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
)
|
||||
}
|
||||
MessageComposerView(
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
|||
|
||||
internal fun aTimelineItemEvent(
|
||||
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
|
||||
transactionId: String? = null,
|
||||
isMine: Boolean = false,
|
||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||
|
|
@ -104,6 +105,7 @@ internal fun aTimelineItemEvent(
|
|||
return TimelineItem.Event(
|
||||
id = eventId.value,
|
||||
eventId = eventId,
|
||||
transactionId = transactionId,
|
||||
senderId = UserId("@senderId:domain"),
|
||||
senderAvatar = AvatarData("@senderId:domain", "sender"),
|
||||
content = content,
|
||||
|
|
|
|||
|
|
@ -68,10 +68,11 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
fun TimelineView(
|
||||
state: TimelineState,
|
||||
onUserDataClicked: (UserId) -> Unit,
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit,
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onUserDataClicked: (UserId) -> Unit = {},
|
||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||
) {
|
||||
fun onReachedLoadMore() {
|
||||
state.eventSink(TimelineEvents.LoadMore)
|
||||
|
|
@ -102,6 +103,7 @@ fun TimelineView(
|
|||
onLongClick = onMessageLongClicked,
|
||||
onUserDataClick = onUserDataClicked,
|
||||
inReplyToClick = ::inReplyToClicked,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
)
|
||||
if (index == state.timelineItems.lastIndex) {
|
||||
onReachedLoadMore()
|
||||
|
|
@ -125,6 +127,7 @@ fun TimelineItemRow(
|
|||
onClick: (TimelineItem.Event) -> Unit,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
inReplyToClick: (EventId) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
when (timelineItem) {
|
||||
|
|
@ -159,6 +162,7 @@ fun TimelineItemRow(
|
|||
onLongClick = ::onLongClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -191,6 +195,7 @@ fun TimelineItemRow(
|
|||
onLongClick = onLongClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -276,6 +281,10 @@ fun TimelineViewDarkPreview(
|
|||
private fun ContentToPreview(content: TimelineItemEventContent) {
|
||||
val timelineItems = aTimelineItemList(content)
|
||||
TimelineView(
|
||||
state = aTimelineState(timelineItems)
|
||||
state = aTimelineState(timelineItems),
|
||||
onMessageClicked = {},
|
||||
onTimestampClicked = {},
|
||||
onUserDataClicked = {},
|
||||
onMessageLongClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,15 @@
|
|||
package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -54,7 +57,15 @@ fun TimelineEventTimestampView(
|
|||
val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
|
||||
val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null
|
||||
Row(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
enabled = true,
|
||||
indication = rememberRipple(bounded = false),
|
||||
interactionSource = MutableInteractionSource()
|
||||
)
|
||||
.padding(start = 16.dp) // Add extra padding for touch target size
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (isMessageEdited) {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ fun TimelineItemEventRow(
|
|||
onLongClick: () -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
inReplyToClick: (EventId) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
|
@ -83,7 +84,7 @@ fun TimelineItemEventRow(
|
|||
onUserDataClick(event.senderId)
|
||||
}
|
||||
|
||||
fun inReplayToClicked() {
|
||||
fun inReplyToClicked() {
|
||||
val inReplyToEventId = (event.inReplyTo as? InReplyTo.Ready)?.eventId ?: return
|
||||
inReplyToClick(inReplyToEventId)
|
||||
}
|
||||
|
|
@ -131,7 +132,10 @@ fun TimelineItemEventRow(
|
|||
interactionSource = interactionSource,
|
||||
onMessageClick = onClick,
|
||||
onMessageLongClick = onLongClick,
|
||||
inReplyToClick = ::inReplayToClicked,
|
||||
inReplyToClick = ::inReplyToClicked,
|
||||
onTimestampClicked = {
|
||||
onTimestampClicked(event)
|
||||
}
|
||||
)
|
||||
}
|
||||
TimelineItemReactionsView(
|
||||
|
|
@ -177,6 +181,7 @@ private fun MessageEventBubbleContent(
|
|||
onMessageClick: () -> Unit,
|
||||
onMessageLongClick: () -> Unit,
|
||||
inReplyToClick: () -> Unit,
|
||||
onTimestampClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isMediaItem = event.content is TimelineItemImageContent || event.content is TimelineItemVideoContent
|
||||
|
|
@ -207,7 +212,7 @@ private fun MessageEventBubbleContent(
|
|||
ContentView(modifier = contentModifier)
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onMessageClick,
|
||||
onClick = onTimestampClicked,
|
||||
modifier = timestampModifier
|
||||
.padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
|
||||
.background(LocalColors.current.gray300, RoundedCornerShape(10.0.dp))
|
||||
|
|
@ -220,7 +225,7 @@ private fun MessageEventBubbleContent(
|
|||
ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp))
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onMessageClick,
|
||||
onClick = onTimestampClicked,
|
||||
modifier = timestampModifier
|
||||
.align(Alignment.End)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
|
||||
sealed interface RetrySendMenuEvents {
|
||||
data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents
|
||||
object RetrySend : RetrySendMenuEvents
|
||||
object RemoveFailed : RetrySendMenuEvents
|
||||
object Dismiss: RetrySendMenuEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class RetrySendMenuPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
) : Presenter<RetrySendMenuState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): RetrySendMenuState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var selectedEvent: TimelineItem.Event? by remember { mutableStateOf(null) }
|
||||
|
||||
fun handleEvent(event: RetrySendMenuEvents) {
|
||||
when (event) {
|
||||
is RetrySendMenuEvents.EventSelected -> {
|
||||
selectedEvent = event.event
|
||||
}
|
||||
RetrySendMenuEvents.RetrySend -> {
|
||||
coroutineScope.launch {
|
||||
selectedEvent?.transactionId?.let { transactionId ->
|
||||
room.retrySendMessage(transactionId)
|
||||
}
|
||||
selectedEvent = null
|
||||
}
|
||||
}
|
||||
RetrySendMenuEvents.RemoveFailed -> {
|
||||
coroutineScope.launch {
|
||||
selectedEvent?.transactionId?.let { transactionId ->
|
||||
room.cancelSend(transactionId)
|
||||
}
|
||||
selectedEvent = null
|
||||
}
|
||||
}
|
||||
RetrySendMenuEvents.Dismiss -> {
|
||||
selectedEvent = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RetrySendMenuState(
|
||||
selectedEvent = selectedEvent,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
|
||||
@Immutable
|
||||
data class RetrySendMenuState(
|
||||
val selectedEvent: TimelineItem.Event?,
|
||||
val eventSink: (RetrySendMenuEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
|
||||
class RetrySendMenuStateProvider : PreviewParameterProvider<RetrySendMenuState> {
|
||||
override val values: Sequence<RetrySendMenuState> = sequenceOf(
|
||||
aRetrySendMenuState(event = null),
|
||||
aRetrySendMenuState(event = aTimelineItemEvent()),
|
||||
)
|
||||
}
|
||||
|
||||
fun aRetrySendMenuState(event: TimelineItem.Event? = aTimelineItemEvent()) =
|
||||
RetrySendMenuState(selectedEvent = event, eventSink = {})
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.features.messages.impl.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun RetrySendMessageMenu(
|
||||
state: RetrySendMenuState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isVisible = state.selectedEvent != null
|
||||
|
||||
fun onDismiss() {
|
||||
state.eventSink(RetrySendMenuEvents.Dismiss)
|
||||
}
|
||||
|
||||
fun onRetry() {
|
||||
state.eventSink(RetrySendMenuEvents.RetrySend)
|
||||
}
|
||||
|
||||
fun onRemoveFailed() {
|
||||
state.eventSink(RetrySendMenuEvents.RemoveFailed)
|
||||
}
|
||||
|
||||
RetrySendMessageMenuBottomSheet(
|
||||
modifier = modifier,
|
||||
isVisible = isVisible,
|
||||
onRetry = ::onRetry,
|
||||
onRemoveFailed = ::onRemoveFailed,
|
||||
onDismiss = ::onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun RetrySendMessageMenuBottomSheet(
|
||||
isVisible: Boolean,
|
||||
onRetry: () -> Unit,
|
||||
onRemoveFailed: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
if (isVisible) {
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
// modifier = modifier.navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
|
||||
// .imePadding()
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
coroutineScope.launch {
|
||||
sheetState.hide()
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
) {
|
||||
RetrySendMenuContents(onRetry = onRetry, onRemoveFailed = onRemoveFailed)
|
||||
// FIXME remove after https://issuetracker.google.com/issues/275849044
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ColumnScope.RetrySendMenuContents(
|
||||
onRetry: () -> Unit,
|
||||
onRemoveFailed: () -> Unit,
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
ListItem(headlineContent = {
|
||||
Text(stringResource(R.string.screen_room_retry_send_menu_title), fontWeight = FontWeight.Medium)
|
||||
})
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(stringResource(R.string.screen_room_retry_send_menu_send_again_action))
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
coroutineScope.launch {
|
||||
sheetState.hide()
|
||||
onRetry()
|
||||
}
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(stringResource(R.string.screen_room_retry_send_menu_remove_action))
|
||||
},
|
||||
colors = ListItemDefaults.colors(headlineColor = LocalColors.current.textActionCritical),
|
||||
modifier = Modifier.clickable {
|
||||
coroutineScope.launch {
|
||||
sheetState.hide()
|
||||
onRemoveFailed()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RetrySendMessageMenuPreviewLight(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) {
|
||||
ElementPreviewLight {
|
||||
ContentToPreview(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RetrySendMessageMenuPreviewDark(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) {
|
||||
ElementPreviewDark {
|
||||
ContentToPreview(state)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ContentToPreview(state: RetrySendMenuState) {
|
||||
// TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed
|
||||
Column {
|
||||
RetrySendMenuContents(
|
||||
onRetry = {},
|
||||
onRemoveFailed = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +72,7 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
return TimelineItem.Event(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
eventId = currentTimelineItem.eventId,
|
||||
transactionId = currentTimelineItem.transactionId,
|
||||
senderId = currentSender,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderAvatar = senderAvatarData,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ sealed interface TimelineItem {
|
|||
data class Event(
|
||||
val id: String,
|
||||
val eventId: EventId? = null,
|
||||
val transactionId: String? = null,
|
||||
val senderId: UserId,
|
||||
val senderDisplayName: String?,
|
||||
val senderAvatar: AvatarData,
|
||||
|
|
|
|||
|
|
@ -10,5 +10,8 @@
|
|||
<string name="screen_room_attachment_source_files">"Attachment"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Photo & Video Library"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Your message failed to send"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_room_retry_send_menu_remove_action">"Remove"</string>
|
||||
</resources>
|
||||
|
|
@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
|||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
|
|
@ -322,15 +323,18 @@ class MessagesPresenterTest {
|
|||
flavorShortDescription = "",
|
||||
)
|
||||
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
||||
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
||||
return MessagesPresenter(
|
||||
room = matrixRoom,
|
||||
composerPresenter = messageComposerPresenter,
|
||||
timelinePresenter = timelinePresenter,
|
||||
actionListPresenter = actionListPresenter,
|
||||
retrySendMenuPresenter = retrySendMenuPresenter,
|
||||
networkMonitor = FakeNetworkMonitor(),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.timeline.components.retrysendmenu
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
||||
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class RetrySendMenuPresenterTests {
|
||||
|
||||
private val room = FakeMatrixRoom()
|
||||
private val presenter = RetrySendMenuPresenter(room)
|
||||
|
||||
@Test
|
||||
fun `present - handle event selected`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val selectedEvent = aTimelineItemEvent()
|
||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||
|
||||
assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle dismiss`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val selectedEvent = aTimelineItemEvent()
|
||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RetrySendMenuEvents.Dismiss)
|
||||
assertThat(awaitItem().selectedEvent).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle resend with transactionId`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RetrySendMenuEvents.RetrySend)
|
||||
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
||||
assertThat(awaitItem().selectedEvent).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle resend without transactionId`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RetrySendMenuEvents.RetrySend)
|
||||
assertThat(room.retrySendMessageCount).isEqualTo(0)
|
||||
assertThat(awaitItem().selectedEvent).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle resend with error`() = runTest {
|
||||
room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error")))
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RetrySendMenuEvents.RetrySend)
|
||||
assertThat(room.retrySendMessageCount).isEqualTo(1)
|
||||
assertThat(awaitItem().selectedEvent).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle remove failed message with transactionId`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RetrySendMenuEvents.RemoveFailed)
|
||||
assertThat(room.cancelSendCount).isEqualTo(1)
|
||||
assertThat(awaitItem().selectedEvent).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle remove failed message without transactionId`() = runTest {
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val selectedEvent = aTimelineItemEvent(transactionId = null)
|
||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RetrySendMenuEvents.RemoveFailed)
|
||||
assertThat(room.cancelSendCount).isEqualTo(0)
|
||||
assertThat(awaitItem().selectedEvent).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle remove failed message with error`() = runTest {
|
||||
room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error")))
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(RetrySendMenuEvents.RemoveFailed)
|
||||
assertThat(room.cancelSendCount).isEqualTo(1)
|
||||
assertThat(awaitItem().selectedEvent).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,10 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun sendReaction(emoji: String, eventId: EventId): Result<Unit>
|
||||
|
||||
suspend fun retrySendMessage(transactionId: String): Result<Unit>
|
||||
|
||||
suspend fun cancelSend(transactionId: String): Result<Unit>
|
||||
|
||||
suspend fun leave(): Result<Unit>
|
||||
|
||||
suspend fun acceptInvitation(): Result<Unit>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ sealed interface MatrixTimelineItem {
|
|||
data class Event(val event: EventTimelineItem) : MatrixTimelineItem {
|
||||
val uniqueId: String = event.uniqueIdentifier
|
||||
val eventId: EventId? = event.eventId
|
||||
val transactionId: String? = event.transactionId
|
||||
}
|
||||
|
||||
data class Virtual(val virtual: VirtualTimelineItem) : MatrixTimelineItem
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugIn
|
|||
data class EventTimelineItem(
|
||||
val uniqueIdentifier: String,
|
||||
val eventId: EventId?,
|
||||
val transactionId: String?,
|
||||
val isEditable: Boolean,
|
||||
val isLocal: Boolean,
|
||||
val isOwn: Boolean,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.impl.media.map
|
|||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -264,6 +265,20 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun retrySendMessage(transactionId: String): Result<Unit> =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.retrySend(transactionId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun cancelSend(transactionId: String): Result<Unit> =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.cancelSend(transactionId)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
|
|||
EventTimelineItem(
|
||||
uniqueIdentifier = it.uniqueIdentifier(),
|
||||
eventId = it.eventId()?.let(::EventId),
|
||||
transactionId = it.transactionId(),
|
||||
isEditable = it.isEditable(),
|
||||
isLocal = it.isLocal(),
|
||||
isOwn = it.isOwn(),
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ val A_ROOM_ID_2 = RoomId("!aRoomId2:domain")
|
|||
val A_THREAD_ID = ThreadId("\$aThreadId")
|
||||
val AN_EVENT_ID = EventId("\$anEventId")
|
||||
val AN_EVENT_ID_2 = EventId("\$anEventId2")
|
||||
const val A_TRANSACTION_ID = "aTransactionId"
|
||||
const val A_UNIQUE_ID = "aUniqueId"
|
||||
|
||||
const val A_ROOM_NAME = "A room name"
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ class FakeMatrixRoom(
|
|||
private var updateAvatarResult = Result.success(Unit)
|
||||
private var removeAvatarResult = Result.success(Unit)
|
||||
private var sendReactionResult = Result.success(Unit)
|
||||
private var retrySendMessageResult = Result.success(Unit)
|
||||
private var cancelSendResult = Result.success(Unit)
|
||||
|
||||
var sendMediaCount = 0
|
||||
private set
|
||||
|
|
@ -78,6 +80,12 @@ class FakeMatrixRoom(
|
|||
var sendReactionCount = 0
|
||||
private set
|
||||
|
||||
var retrySendMessageCount: Int = 0
|
||||
private set
|
||||
|
||||
var cancelSendCount: Int = 0
|
||||
private set
|
||||
|
||||
var isInviteAccepted: Boolean = false
|
||||
private set
|
||||
|
||||
|
|
@ -133,6 +141,16 @@ class FakeMatrixRoom(
|
|||
return sendReactionResult
|
||||
}
|
||||
|
||||
override suspend fun retrySendMessage(transactionId: String): Result<Unit> {
|
||||
retrySendMessageCount++
|
||||
return retrySendMessageResult
|
||||
}
|
||||
|
||||
override suspend fun cancelSend(transactionId: String): Result<Unit> {
|
||||
cancelSendCount++
|
||||
return cancelSendResult
|
||||
}
|
||||
|
||||
var editMessageParameter: String? = null
|
||||
private set
|
||||
|
||||
|
|
@ -292,4 +310,12 @@ class FakeMatrixRoom(
|
|||
fun givenSendReactionResult(result: Result<Unit>) {
|
||||
sendReactionResult = result
|
||||
}
|
||||
|
||||
fun givenRetrySendMessageResult(result: Result<Unit>) {
|
||||
retrySendMessageResult = result
|
||||
}
|
||||
|
||||
fun givenCancelSendResult(result: Result<Unit>) {
|
||||
cancelSendResult = result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ fun aRoomMessage(
|
|||
fun anEventTimelineItem(
|
||||
uniqueIdentifier: String = A_UNIQUE_ID,
|
||||
eventId: EventId = AN_EVENT_ID,
|
||||
transactionId: String? = null,
|
||||
isEditable: Boolean = false,
|
||||
isLocal: Boolean = false,
|
||||
isOwn: Boolean = false,
|
||||
|
|
@ -103,6 +104,7 @@ fun anEventTimelineItem(
|
|||
) = EventTimelineItem(
|
||||
uniqueIdentifier = uniqueIdentifier,
|
||||
eventId = eventId,
|
||||
transactionId = transactionId,
|
||||
isEditable = isEditable,
|
||||
isLocal = isLocal,
|
||||
isOwn = isOwn,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:307bca06295570124970ed808f4a068f50086212623fa0e69630f52cf9e8752a
|
||||
size 15440
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:307bca06295570124970ed808f4a068f50086212623fa0e69630f52cf9e8752a
|
||||
size 15440
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e47e880d5bf8ffedd0662ec0c791483a2cb76e921d12958afffa959b1370c270
|
||||
size 14073
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e47e880d5bf8ffedd0662ec0c791483a2cb76e921d12958afffa959b1370c270
|
||||
size 14073
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bdf7fe891aa40f4d626733deb130481d64b3315531712128d1b4073e5ccedf19
|
||||
size 5394
|
||||
oid sha256:0f56dcf7dfb9ca58618e891aae28099a82457b889da3cd8ec613aeb3757e9750
|
||||
size 5403
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0baf33f0ec99cd8e4b201d72b38208a02b0511da544276a38d17ff5653e2b754
|
||||
size 5906
|
||||
oid sha256:b0c0973dd677105248cd17351ac32e9cf79dc6082eb3a9e26a7ff37e2475ca2f
|
||||
size 5860
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c8d9dc01ec07f9e43032e3ec7610eda5588145f3003cc3d329ddf4325e19f239
|
||||
size 6674
|
||||
oid sha256:2fd42d7ae900d5ece9499b1877766d82d8d174d4b8b04633c93e1de04d3aa069
|
||||
size 6715
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e0f86449d651c50966fe594a46e3a59ae9a5505013e3e36f2458be35ba1844e
|
||||
size 7174
|
||||
oid sha256:e324ac2b0578de021d27cddbba851d5835086f80066cb53c1c1829241ac63fec
|
||||
size 7172
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:621facc4799c0069fe83b4be9a5085ecafb042de1cdfbbf4ccf0c15548373aa9
|
||||
size 5332
|
||||
oid sha256:fc5e3c5e68dcab10bd67878cd28d1cf769b19cb4daa3dfc02a310b9e44e38d59
|
||||
size 5362
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:986f40750d69639e771c9bf1aac30c49dbf122aa60dc0280164a8868671b638a
|
||||
size 5910
|
||||
oid sha256:8ec5e0cbd36f80526f2a55637d35c41eec7cd8dd07562bc9ed115b2886963b39
|
||||
size 5888
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6d4fb8d68603309963d434f3166821055d818836b1ebfb4fc4c637aba992277d
|
||||
size 6617
|
||||
oid sha256:961c557c4d7f77a6d1deb9bbb908cdd855f5e3ebee91e211c91afe857ba3785d
|
||||
size 6593
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1fa641ca8d5c796c858b54ad997052aabf817bc86e254d38d4b33038bc21eedf
|
||||
size 7285
|
||||
oid sha256:833ccceceff1f0a74db95058f3714da514525d05cd3e040619fb221e32ba8d33
|
||||
size 7287
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:052a95ccf790c0ef93bbfd24ff063fd5489b2c250a9c53ab6d58438653fbbea5
|
||||
size 46098
|
||||
oid sha256:259022501a602e5e6e22a800f866d221500f34f7ce475df9996b897f93e7fc49
|
||||
size 45929
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:744f2192c85b1d6a56cc2eb7f6c873eb45827d2ad2d36fd1678c4ace4118af2c
|
||||
size 45945
|
||||
oid sha256:4fe9d2349725bb5c6dbfcfd29dcf8c7c10079deefdc8a0cb367dfe23cb453b85
|
||||
size 45873
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue