diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 06643f616c..b91cf18464 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -41,6 +41,7 @@ import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode +import io.element.android.appnav.loggedin.SendingQueue import io.element.android.appnav.room.RoomFlowNode import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode @@ -102,6 +103,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint, private val shareEntryPoint: ShareEntryPoint, private val matrixClient: MatrixClient, + private val sendingQueue: SendingQueue, snackbarDispatcher: SnackbarDispatcher, ) : BaseFlowNode( backstack = BackStack( @@ -157,6 +159,11 @@ class LoggedInFlowNode @AssistedInject constructor( } ) observeSyncStateAndNetworkStatus() + setupSendingQueue() + } + + private fun setupSendingQueue() { + sendingQueue.launchIn(lifecycleScope) } @OptIn(FlowPreview::class) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendingQueue.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendingQueue.kt new file mode 100644 index 0000000000..979683de96 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SendingQueue.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 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.appnav.loggedin + +import androidx.annotation.VisibleForTesting +import io.element.android.features.networkmonitor.api.NetworkMonitor +import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import java.util.concurrent.atomic.AtomicInteger +import javax.inject.Inject + +private const val SENDING_QUEUE_MIN_RETRY_DELAY = 250L + +@VisibleForTesting +const val SENDING_QUEUE_MAX_RETRY_DELAY = 5000L + +@SingleIn(SessionScope::class) +class SendingQueue @Inject constructor( + private val matrixClient: MatrixClient, + private val networkMonitor: NetworkMonitor, +) { + private val retryCount = AtomicInteger(0) + + fun launchIn(coroutineScope: CoroutineScope) { + combine( + networkMonitor.connectivity, + matrixClient.sendingQueueStatus(), + ) { networkStatus, isSendingQueueEnabled -> + Pair(networkStatus, isSendingQueueEnabled) + }.onEach { (networkStatus, isSendingQueueEnabled) -> + Timber.d("Network status: $networkStatus, isSendingQueueEnabled: $isSendingQueueEnabled") + if (networkStatus == NetworkStatus.Online && !isSendingQueueEnabled) { + val retryDelay = + (SENDING_QUEUE_MIN_RETRY_DELAY * retryCount.incrementAndGet()).coerceIn(SENDING_QUEUE_MIN_RETRY_DELAY, SENDING_QUEUE_MAX_RETRY_DELAY) + Timber.d("Retry enabling sending queue in $retryDelay ms") + delay(retryDelay) + } else { + retryCount.set(0) + } + matrixClient.setSendingQueueEnabled(enabled = networkStatus == NetworkStatus.Online) + }.launchIn(coroutineScope) + } +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendingQueueTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendingQueueTest.kt new file mode 100644 index 0000000000..a31ef0efc3 --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/SendingQueueTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 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.appnav.loggedin + +import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) class SendingQueueTest { + private val matrixClient = FakeMatrixClient() + private val networkMonitor = FakeNetworkMonitor() + private val sut = SendingQueue(matrixClient, networkMonitor) + + @Test + fun `test network status online and sending queue is disabled`() = runTest { + val sendingQueueStatusFlow = MutableStateFlow(false) + val setEnableSendingQueueLambda = lambdaRecorder { _: Boolean -> } + matrixClient.sendingQueueStatusFlow = sendingQueueStatusFlow + matrixClient.setSendingQueueEnabledLambda = setEnableSendingQueueLambda + + sut.launchIn(backgroundScope) + + advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY) + sendingQueueStatusFlow.value = true + advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY) + + assert(setEnableSendingQueueLambda) + .isCalledExactly(2) + .withSequence( + listOf(value(true)), + listOf(value(true)) + ) + } + + @Test + fun `test network status getting offline and online`() = runTest { + val sendingQueueStatusFlow = MutableStateFlow(true) + val setEnableSendingQueueLambda = lambdaRecorder { _: Boolean -> } + matrixClient.sendingQueueStatusFlow = sendingQueueStatusFlow + matrixClient.setSendingQueueEnabledLambda = setEnableSendingQueueLambda + + sut.launchIn(backgroundScope) + advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY) + networkMonitor.connectivity.value = NetworkStatus.Offline + advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY) + networkMonitor.connectivity.value = NetworkStatus.Online + advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY) + + assert(setEnableSendingQueueLambda) + .isCalledExactly(3) + .withSequence( + listOf(value(true)), + listOf(value(false)), + listOf(value(true)), + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 72b05a62cc..065c81e407 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -46,7 +46,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter -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.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent @@ -107,7 +106,6 @@ class MessagesPresenter @AssistedInject constructor( private val actionListPresenter: ActionListPresenter, private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, - private val retrySendMenuPresenter: RetrySendMenuPresenter, private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, @@ -140,7 +138,6 @@ class MessagesPresenter @AssistedInject constructor( val actionListState = actionListPresenter.present() val customReactionState = customReactionPresenter.present() val reactionSummaryState = reactionSummaryPresenter.present() - val retryState = retrySendMenuPresenter.present() val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() @@ -231,7 +228,6 @@ class MessagesPresenter @AssistedInject constructor( actionListState = actionListState, customReactionState = customReactionState, reactionSummaryState = reactionSummaryState, - retrySendMenuState = retryState, readReceiptBottomSheetState = readReceiptBottomSheetState, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, snackbarMessage = snackbarMessage, @@ -309,11 +305,9 @@ class MessagesPresenter @AssistedInject constructor( } private suspend fun handleActionRedact(event: TimelineItem.Event) { - if (event.failedToSend) { - // If the message hasn't been sent yet, just cancel it - event.transactionId?.let { room.cancelSend(it) } - } else if (event.eventId != null) { - room.redactEvent(event.eventId) + timelineController.invokeOnCurrentTimeline { + redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null) + .onFailure { Timber.e(it) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 5c6e1c7ffa..752aa94a9f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -23,7 +23,6 @@ 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.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.libraries.architecture.AsyncData @@ -47,7 +46,6 @@ data class MessagesState( val actionListState: ActionListState, val customReactionState: CustomReactionState, val reactionSummaryState: ReactionSummaryState, - val retrySendMenuState: RetrySendMenuState, val readReceiptBottomSheetState: ReadReceiptBottomSheetState, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index fd00eba04b..526edfb1e7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -31,8 +31,6 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState -import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState -import io.element.android.features.messages.impl.timeline.components.retrysendmenu.aRetrySendMenuState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.typing.aTypingNotificationState @@ -110,7 +108,6 @@ fun aMessagesState( // Render a focused event for an event with sender information displayed focusedEventIndex = 2, ), - retrySendMenuState: RetrySendMenuState = aRetrySendMenuState(), readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(), actionListState: ActionListState = anActionListState(), customReactionState: CustomReactionState = aCustomReactionState(), @@ -132,7 +129,6 @@ fun aMessagesState( voiceMessageComposerState = voiceMessageComposerState, typingNotificationState = aTypingNotificationState(), timelineState = timelineState, - retrySendMenuState = retrySendMenuState, readReceiptBottomSheetState = readReceiptBottomSheetState, actionListState = actionListState, customReactionState = customReactionState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 702dffd607..b9b9e7e4a7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -74,8 +74,6 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents -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.messages.impl.voicemessages.composer.VoiceMessageComposerEvents import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog @@ -103,7 +101,6 @@ import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import timber.log.Timber @@ -212,11 +209,6 @@ fun MessagesView( onMessageLongClick = ::onMessageLongClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, - onTimestampClick = { event -> - if (event.localSendState is LocalEventSendState.SendingFailed) { - state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event)) - } - }, onReactionClick = ::onEmojiReactionClick, onReactionLongClick = ::onEmojiReactionLongClick, onMoreReactionsClick = ::onMoreReactionsClick, @@ -258,7 +250,6 @@ fun MessagesView( ) ReactionSummaryView(state = state.reactionSummaryState) - RetrySendMessageMenu(state = state.retrySendMenuState) ReadReceiptBottomSheet( state = state.readReceiptBottomSheetState, onUserDataClick = onUserDataClick, @@ -319,7 +310,6 @@ private fun MessagesViewContent( onMoreReactionsClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit, onMessageLongClick: (TimelineItem.Event) -> Unit, - onTimestampClick: (TimelineItem.Event) -> Unit, onSendLocationClick: () -> Unit, onCreatePollClick: () -> Unit, onJoinCallClick: () -> Unit, @@ -387,7 +377,6 @@ private fun MessagesViewContent( onLinkClick = onLinkClick, onMessageClick = onMessageClick, onMessageLongClick = onMessageLongClick, - onTimestampClick = onTimestampClick, onSwipeToReply = onSwipeToReply, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 609ab5c3b3..a3452d4422 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -181,7 +181,7 @@ class ActionListPresenter @Inject constructor( add(TimelineItemAction.Forward) } } - if (timelineItem.isMine && timelineItem.isTextMessage) { + if (timelineItem.isEditable) { add(TimelineItemAction.Edit) } if (timelineItem.content.canBeCopied()) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 19df330c54..370d7bc517 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -83,7 +83,6 @@ fun TimelineView( onLinkClick: (String) -> Unit, onMessageClick: (TimelineItem.Event) -> Unit, onMessageLongClick: (TimelineItem.Event) -> Unit, - onTimestampClick: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit, onReactionClick: (emoji: String, TimelineItem.Event) -> Unit, onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit, @@ -148,7 +147,6 @@ fun TimelineView( onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, - onTimestampClick = onTimestampClick, eventSink = state.eventSink, onSwipeToReply = onSwipeToReply, onJoinCallClick = onJoinCallClick, @@ -245,8 +243,8 @@ private fun BoxScope.TimelineScrollHelper( // Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive, modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 24.dp, bottom = 12.dp), + .align(Alignment.BottomEnd) + .padding(end = 24.dp, bottom = 12.dp), onClick = { jumpToBottom() }, ) } @@ -273,8 +271,8 @@ private fun JumpToBottomButton( ) { Icon( modifier = Modifier - .size(24.dp) - .rotate(90f), + .size(24.dp) + .rotate(90f), imageVector = CompoundIcons.ArrowRight(), contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom) ) @@ -301,7 +299,6 @@ internal fun TimelineViewPreview( onLinkClick = {}, onMessageClick = {}, onMessageLongClick = {}, - onTimestampClick = {}, onSwipeToReply = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index b6e6646c1e..8e482c020f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -45,6 +45,5 @@ internal fun ATimelineItemEventRow( onMoreReactionsClick = {}, onReadReceiptClick = {}, onSwipeToReply = {}, - onTimestampClick = {}, eventSink = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index 9206369a1e..5ec0262205 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -16,61 +16,34 @@ package io.element.android.features.messages.impl.timeline.components -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.PaddingValues 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.ripple.rememberRipple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.isEdited import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.ui.strings.CommonStrings -@OptIn(ExperimentalFoundationApi::class) @Composable fun TimelineEventTimestampView( - event: TimelineItem.Event, - onClick: () -> Unit, - onLongClick: () -> Unit, + formattedTime: String, + isMessageEdited: Boolean, modifier: Modifier = Modifier, ) { - val formattedTime = event.sentTime - val hasMessageSendingFailed = event.localSendState is LocalEventSendState.SendingFailed - val isMessageEdited = event.content.isEdited() - val tint = if (hasMessageSendingFailed) MaterialTheme.colorScheme.error else null - val clickModifier = if (hasMessageSendingFailed) { - Modifier.combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - indication = rememberRipple(bounded = false), - interactionSource = remember { MutableInteractionSource() } - ) - } else { - Modifier - } Row( modifier = Modifier - .then(clickModifier) - // Add extra padding for touch target size .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)) .then(modifier), verticalAlignment = Alignment.CenterVertically, @@ -79,35 +52,22 @@ fun TimelineEventTimestampView( Text( stringResource(CommonStrings.common_edited_suffix), style = ElementTheme.typography.fontBodyXsRegular, - color = tint ?: MaterialTheme.colorScheme.secondary, + color = MaterialTheme.colorScheme.secondary, ) Spacer(modifier = Modifier.width(4.dp)) } Text( formattedTime, style = ElementTheme.typography.fontBodyXsRegular, - color = tint ?: MaterialTheme.colorScheme.secondary, + color = MaterialTheme.colorScheme.secondary, ) - if (hasMessageSendingFailed && tint != null) { - Spacer(modifier = Modifier.width(2.dp)) - Icon( - imageVector = CompoundIcons.Error(), - contentDescription = stringResource(id = CommonStrings.common_sending_failed), - tint = tint, - modifier = Modifier.size(15.dp, 18.dp), - ) - } } } @PreviewsDayNight @Composable internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview { - TimelineEventTimestampView( - event = event, - onClick = {}, - onLongClick = {}, - ) + TimelineEventTimestampView(formattedTime = event.sentTime, isMessageEdited = event.content.isEdited()) } object TimelineEventTimestampViewDefaults { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 993d872852..0c5dec2553 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -92,6 +92,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo +import io.element.android.features.messages.impl.timeline.model.event.isEdited import io.element.android.features.messages.impl.timeline.model.eventId import io.element.android.features.messages.impl.timeline.model.metadata import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom @@ -130,7 +131,6 @@ fun TimelineItemEventRow( onLinkClick: (String) -> Unit, onUserDataClick: (UserId) -> Unit, inReplyToClick: (EventId) -> Unit, - onTimestampClick: (TimelineItem.Event) -> Unit, onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit, @@ -190,7 +190,6 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, - onTimestampClick = onTimestampClick, inReplyToClick = ::inReplyToClick, onUserDataClick = ::onUserDataClick, onReactionClick = { emoji -> onReactionClick(emoji, event) }, @@ -209,7 +208,6 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, - onTimestampClick = onTimestampClick, inReplyToClick = ::inReplyToClick, onUserDataClick = ::onUserDataClick, onReactionClick = { emoji -> onReactionClick(emoji, event) }, @@ -265,7 +263,6 @@ private fun TimelineItemEventRowContent( interactionSource: MutableInteractionSource, onClick: () -> Unit, onLongClick: () -> Unit, - onTimestampClick: (TimelineItem.Event) -> Unit, inReplyToClick: () -> Unit, onUserDataClick: () -> Unit, onReactionClick: (emoji: String) -> Unit, @@ -337,9 +334,6 @@ private fun TimelineItemEventRowContent( event = event, onMessageLongClick = onLongClick, inReplyToClick = inReplyToClick, - onTimestampClick = { - onTimestampClick(event) - }, onLinkClick = onLinkClick, eventSink = eventSink, ) @@ -419,7 +413,6 @@ private fun MessageEventBubbleContent( event: TimelineItem.Event, onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, - onTimestampClick: () -> Unit, onLinkClick: (String) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, @SuppressLint("ModifierParameter") @@ -467,9 +460,8 @@ private fun MessageEventBubbleContent( Box(modifier, contentAlignment = Alignment.Center) { content {} TimelineEventTimestampView( - event = event, - onClick = onTimestampClick, - onLongClick = ::onTimestampLongClick, + formattedTime = event.sentTime, + isMessageEdited = event.content.isEdited(), modifier = Modifier // Outer padding .padding(horizontal = 4.dp, vertical = 4.dp) @@ -489,9 +481,8 @@ private fun MessageEventBubbleContent( content = { content(this::onContentLayoutChange) }, overlay = { TimelineEventTimestampView( - event = event, - onClick = onTimestampClick, - onLongClick = ::onTimestampLongClick, + formattedTime = event.sentTime, + isMessageEdited = event.content.isEdited(), modifier = Modifier .padding(horizontal = 8.dp, vertical = 4.dp) ) @@ -501,9 +492,8 @@ private fun MessageEventBubbleContent( Column(modifier) { content {} TimelineEventTimestampView( - event = event, - onClick = onTimestampClick, - onLongClick = ::onTimestampLongClick, + formattedTime = event.sentTime, + isMessageEdited = event.content.isEdited(), modifier = Modifier .align(Alignment.End) .padding(horizontal = 8.dp, vertical = 4.dp) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index e50ea0ee8c..4dd4d4e356 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -49,7 +49,6 @@ fun TimelineItemGroupedEventsRow( inReplyToClick: (EventId) -> Unit, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, - onTimestampClick: (TimelineItem.Event) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit, @@ -76,7 +75,6 @@ fun TimelineItemGroupedEventsRow( inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, - onTimestampClick = onTimestampClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, @@ -100,7 +98,6 @@ private fun TimelineItemGroupedEventsRowContent( inReplyToClick: (EventId) -> Unit, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, - onTimestampClick: (TimelineItem.Event) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit, @@ -133,7 +130,6 @@ private fun TimelineItemGroupedEventsRowContent( inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, - onTimestampClick = onTimestampClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, @@ -175,7 +171,6 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi inReplyToClick = {}, onUserDataClick = {}, onLinkClick = {}, - onTimestampClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, onMoreReactionsClick = {}, @@ -200,7 +195,6 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi inReplyToClick = {}, onUserDataClick = {}, onLinkClick = {}, - onTimestampClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, onMoreReactionsClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 06683d3569..2a3a0305c2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -54,7 +54,6 @@ internal fun TimelineItemRow( onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit, - onTimestampClick: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit, onJoinCallClick: () -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, @@ -118,7 +117,6 @@ internal fun TimelineItemRow( onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, - onTimestampClick = onTimestampClick, onSwipeToReply = { onSwipeToReply(timelineItem) }, eventSink = eventSink, ) @@ -137,7 +135,6 @@ internal fun TimelineItemRow( inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, - onTimestampClick = onTimestampClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt index 3bce2b389b..84f619341c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt @@ -81,7 +81,8 @@ fun TimelineItemReadReceiptView( } } else { when (state.sendState) { - LocalEventSendState.NotSentYet -> { + is LocalEventSendState.SendingFailed, + is LocalEventSendState.NotSentYet -> { ReadReceiptsRow(modifier) { Icon( modifier = Modifier.padding(2.dp), @@ -91,10 +92,6 @@ fun TimelineItemReadReceiptView( ) } } - LocalEventSendState.Canceled -> Unit - is LocalEventSendState.SendingFailed -> { - // Error? The timestamp is already displayed in red - } null, is LocalEventSendState.Sent -> { if (state.isLastOutgoingMessage) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt index 1062e11977..d54d48eeab 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt @@ -123,7 +123,6 @@ private fun ReadReceiptBottomSheetContent( @PreviewsDayNight @Composable internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) = ElementPreview { - // TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed Column { ReadReceiptBottomSheetContent( state = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt deleted file mode 100644 index 77854773c4..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 - data object Retry : RetrySendMenuEvents - data object Remove : RetrySendMenuEvents - data object Dismiss : RetrySendMenuEvents -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt deleted file mode 100644 index 8745df9e53..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 { - @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.Retry -> { - coroutineScope.launch { - selectedEvent?.transactionId?.let { transactionId -> - room.retrySendMessage(transactionId) - } - selectedEvent = null - } - } - RetrySendMenuEvents.Remove -> { - coroutineScope.launch { - selectedEvent?.transactionId?.let { transactionId -> - room.cancelSend(transactionId) - } - selectedEvent = null - } - } - RetrySendMenuEvents.Dismiss -> { - selectedEvent = null - } - } - } - - return RetrySendMenuState( - selectedEvent = selectedEvent, - eventSink = { handleEvent(it) }, - ) - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt deleted file mode 100644 index e10e9c752c..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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, -) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt deleted file mode 100644 index 14353ddd09..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 { - override val values: Sequence = sequenceOf( - aRetrySendMenuState(), - aRetrySendMenuState(event = aTimelineItemEvent()), - ) -} - -fun aRetrySendMenuState( - event: TimelineItem.Event? = null, - eventSink: (RetrySendMenuEvents) -> Unit = {}, -) = RetrySendMenuState( - selectedEvent = event, - eventSink = eventSink, -) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt deleted file mode 100644 index ce01c18dfa..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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.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.MaterialTheme -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.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.features.messages.impl.R -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.ui.strings.CommonStrings -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.Retry) - } - - fun onRemove() { - state.eventSink(RetrySendMenuEvents.Remove) - } - - RetrySendMessageMenuBottomSheet( - modifier = modifier, - isVisible = isVisible, - onRetry = ::onRetry, - onRemove = ::onRemove, - onDismiss = ::onDismiss - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun RetrySendMessageMenuBottomSheet( - isVisible: Boolean, - onRetry: () -> Unit, - onRemove: () -> 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, - onRemove = onRemove, - ) - // 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, - onRemove: () -> Unit, - sheetState: SheetState = rememberModalBottomSheetState(), -) { - val coroutineScope = rememberCoroutineScope() - - ListItem(headlineContent = { - Text( - text = stringResource(R.string.screen_room_retry_send_menu_title), - style = ElementTheme.typography.fontBodyLgMedium, - ) - }) - ListItem( - headlineContent = { - Text( - text = stringResource(R.string.screen_room_retry_send_menu_send_again_action), - style = ElementTheme.typography.fontBodyLgRegular, - ) - }, - modifier = Modifier.clickable { - coroutineScope.launch { - sheetState.hide() - onRetry() - } - } - ) - ListItem( - headlineContent = { - Text( - text = stringResource(CommonStrings.action_remove), - style = ElementTheme.typography.fontBodyLgRegular, - ) - }, - colors = ListItemDefaults.colors(headlineColor = MaterialTheme.colorScheme.error), - modifier = Modifier.clickable { - coroutineScope.launch { - sheetState.hide() - onRemove() - } - } - ) -} - -@PreviewsDayNight -@Composable -internal fun RetrySendMessageMenuPreview(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) = ElementPreview { - RetrySendMessageMenu( - state = state, - ) -} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 1732087814..2fcfe65dd1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -38,7 +38,6 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter -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.TimelineItemTextContent @@ -839,7 +838,6 @@ class MessagesPresenterTest { val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) - val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom) return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, @@ -849,7 +847,6 @@ class MessagesPresenterTest { actionListPresenter = actionListPresenter, customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, - retrySendMenuPresenter = retrySendMenuPresenter, readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 9ae6453a4f..e66bd4c7a1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -106,7 +106,6 @@ private fun AndroidComposeTestRule.setTimel onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(), onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onMessageLongClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), - onTimestampClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onSwipeToReply: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onReactionClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(), onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(), @@ -123,7 +122,6 @@ private fun AndroidComposeTestRule.setTimel onLinkClick = onLinkClick, onMessageClick = onMessageClick, onMessageLongClick = onMessageLongClick, - onTimestampClick = onTimestampClick, onSwipeToReply = onSwipeToReply, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenterTest.kt deleted file mode 100644 index 7881cba6aa..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenterTest.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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 app.cash.molecule.RecompositionMode -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.libraries.matrix.test.A_TRANSACTION_ID -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.tests.testutils.WarmUpRule -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class RetrySendMenuPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - private val room = FakeMatrixRoom() - private val presenter = RetrySendMenuPresenter(room) - - @Test - fun `present - handle event selected`() = runTest { - moleculeFlow(RecompositionMode.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(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val selectedEvent = aTimelineItemEvent() - initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) - skipItems(1) - initialState.eventSink(RetrySendMenuEvents.Dismiss) - assertThat(room.cancelSendCount).isEqualTo(0) - assertThat(room.retrySendMessageCount).isEqualTo(0) - assertThat(awaitItem().selectedEvent).isNull() - } - } - - @Test - fun `present - handle resend with transactionId`() = runTest { - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) - initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) - skipItems(1) - initialState.eventSink(RetrySendMenuEvents.Retry) - assertThat(room.cancelSendCount).isEqualTo(0) - assertThat(room.retrySendMessageCount).isEqualTo(1) - assertThat(awaitItem().selectedEvent).isNull() - } - } - - @Test - fun `present - handle resend without transactionId`() = runTest { - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val selectedEvent = aTimelineItemEvent(transactionId = null) - initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) - skipItems(1) - initialState.eventSink(RetrySendMenuEvents.Retry) - assertThat(room.cancelSendCount).isEqualTo(0) - 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(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) - initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) - skipItems(1) - initialState.eventSink(RetrySendMenuEvents.Retry) - assertThat(room.cancelSendCount).isEqualTo(0) - assertThat(room.retrySendMessageCount).isEqualTo(1) - assertThat(awaitItem().selectedEvent).isNull() - } - } - - @Test - fun `present - handle remove failed message with transactionId`() = runTest { - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) - initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) - skipItems(1) - initialState.eventSink(RetrySendMenuEvents.Remove) - assertThat(room.cancelSendCount).isEqualTo(1) - assertThat(room.retrySendMessageCount).isEqualTo(0) - assertThat(awaitItem().selectedEvent).isNull() - } - } - - @Test - fun `present - handle remove failed message without transactionId`() = runTest { - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val selectedEvent = aTimelineItemEvent(transactionId = null) - initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) - skipItems(1) - initialState.eventSink(RetrySendMenuEvents.Remove) - assertThat(room.cancelSendCount).isEqualTo(0) - assertThat(room.retrySendMessageCount).isEqualTo(0) - assertThat(awaitItem().selectedEvent).isNull() - } - } - - @Test - fun `present - handle remove failed message with error`() = runTest { - room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error"))) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) - initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) - skipItems(1) - initialState.eventSink(RetrySendMenuEvents.Remove) - assertThat(room.cancelSendCount).isEqualTo(1) - assertThat(room.retrySendMessageCount).isEqualTo(0) - assertThat(awaitItem().selectedEvent).isNull() - } - } -} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenuTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenuTest.kt deleted file mode 100644 index 41d6d5610f..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenuTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2024 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.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.element.android.features.messages.impl.R -import io.element.android.features.messages.impl.timeline.aTimelineItemEvent -import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.tests.testutils.EventsRecorder -import io.element.android.tests.testutils.clickOn -import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith -import org.robolectric.annotation.Config - -@RunWith(AndroidJUnit4::class) -class RetrySendMessageMenuTest { - @get:Rule val rule = createAndroidComposeRule() - - @Test - fun `dismiss the bottom sheet emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setRetrySendMessageMenu( - aRetrySendMenuState( - event = aTimelineItemEvent(), - eventSink = eventsRecorder - ), - ) - rule.pressBackKey() - // Cannot test this for now. - // eventsRecorder.assertSingle(RetrySendMenuEvents.Dismiss) - } - - @Config(qualifiers = "h1024dp") - @Test - fun `retry to send the event emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setRetrySendMessageMenu( - aRetrySendMenuState( - event = aTimelineItemEvent(), - eventSink = eventsRecorder - ), - ) - rule.clickOn(R.string.screen_room_retry_send_menu_send_again_action) - eventsRecorder.assertSingle(RetrySendMenuEvents.Retry) - } - - @Config(qualifiers = "h1024dp") - @Test - fun `remove the event emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setRetrySendMessageMenu( - aRetrySendMenuState( - event = aTimelineItemEvent(), - eventSink = eventsRecorder - ), - ) - rule.clickOn(CommonStrings.action_remove) - eventsRecorder.assertSingle(RetrySendMenuEvents.Remove) - } -} - -private fun AndroidComposeTestRule.setRetrySendMessageMenu( - state: RetrySendMenuState, -) { - setContent { - RetrySendMessageMenu( - state = state, - ) - } -} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt index e8d2506408..d1902facb7 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt @@ -68,8 +68,12 @@ class PollRepository @Inject constructor( suspend fun deletePoll( pollStartId: EventId, - ): Result = - room.redactEvent( - eventId = pollStartId, - ) + ): Result = + timelineProvider + .getActiveTimeline() + .redactEvent( + eventId = pollStartId, + transactionId = null, + reason = null, + ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index a4b808d653..6680726672 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -105,4 +105,20 @@ interface MatrixClient : Closeable { suspend fun getRecentlyVisitedRooms(): Result> suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List): Result + + /** + * Enables or disables the sending queue, according to the given parameter. + * + * The sending queue automatically disables itself whenever sending an + * event with it failed (e.g. sending an event via the Timeline), + * so it's required to manually re-enable it as soon as + * connectivity is back on the device. + */ + suspend fun setSendingQueueEnabled(enabled: Boolean) + + /** + * Returns the current status of the sending queue as a [StateFlow]. + * If true, the sending queue is enabled. + */ + fun sendingQueueStatus(): StateFlow } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 7ca6080f11..458b17209c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -130,8 +130,6 @@ interface MatrixRoom : Closeable { suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result - suspend fun redactEvent(eventId: EventId, reason: String? = null): Result - suspend fun sendImage( file: File, thumbnailFile: File?, @@ -160,7 +158,7 @@ interface MatrixRoom : Closeable { suspend fun retrySendMessage(transactionId: TransactionId): Result - suspend fun cancelSend(transactionId: TransactionId): Result + suspend fun cancelSend(transactionId: TransactionId): Result suspend fun leave(): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 5c1da08b65..ee745ea59d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -83,6 +83,8 @@ interface Timeline : AutoCloseable { progressCallback: ProgressCallback? ): Result + suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result + suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result @@ -91,9 +93,7 @@ interface Timeline : AutoCloseable { suspend fun forwardEvent(eventId: EventId, roomIds: List): Result - suspend fun retrySendMessage(transactionId: TransactionId): Result - - suspend fun cancelSend(transactionId: TransactionId): Result + suspend fun cancelSend(transactionId: TransactionId): Result /** * Share a location message in the room. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt index f74ddca3a8..e68ca3fedb 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt @@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.core.EventId @Immutable sealed interface LocalEventSendState { data object NotSentYet : LocalEventSendState - data object Canceled : LocalEventSendState data class SendingFailed( val error: String diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 4319077946..2ffbe5ab94 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -98,6 +98,7 @@ import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.PowerLevels +import org.matrix.rustcomponents.sdk.SendingQueueStatusListener import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.use import timber.log.Timber @@ -551,6 +552,21 @@ class RustMatrixClient( }.distinctUntilChanged() } + override suspend fun setSendingQueueEnabled(enabled: Boolean) = withContext(sessionDispatcher) { + Timber.i("setSendingQueueEnabled($enabled)") + client.enableSendingQueue(enabled) + } + + override fun sendingQueueStatus(): StateFlow = mxCallbackFlow { + client.subscribeToSendingQueueStatus(object : SendingQueueStatusListener { + override fun onValue(newValue: Boolean) { + channel.trySend(newValue) + } + }) + } + .buffer(Channel.UNLIMITED) + .stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = true) + private suspend fun File.getCacheSize( includeCryptoDb: Boolean = false, ): Long = withContext(sessionDispatcher) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index aa7a90c540..4ee8c220f1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -324,12 +324,6 @@ class RustMatrixRoom( return liveTimeline.sendMessage(body, htmlBody, mentions) } - override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) { - runCatching { - innerRoom.redact(eventId.value, reason) - } - } - override suspend fun leave(): Result = withContext(roomDispatcher) { runCatching { innerRoom.leave() @@ -435,10 +429,10 @@ class RustMatrixRoom( } override suspend fun retrySendMessage(transactionId: TransactionId): Result { - return liveTimeline.retrySendMessage(transactionId) + return Result.failure(UnsupportedOperationException("Not supported")) } - override suspend fun cancelSend(transactionId: TransactionId): Result { + override suspend fun cancelSend(transactionId: TransactionId): Result { return liveTimeline.cancelSend(transactionId) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 0a7e67bcf6..593b41deea 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -279,12 +279,32 @@ class RustTimeline( override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result = withContext(dispatcher) { messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content -> - runCatching { + runCatching { inner.send(content) } } } + override suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result = withContext(dispatcher) { + runCatching { + when { + eventId != null -> { + inner.getEventTimelineItemByEventId(eventId.value).use { + inner.redactEvent(item = it, reason = reason) + } + } + transactionId != null -> { + inner.getEventTimelineItemByTransactionId(transactionId.value).use { + inner.redactEvent(item = it, reason = reason) + } + } + else -> { + error("Either eventId or transactionId must be non-null") + } + } + } + } + override suspend fun editMessage( originalEventId: EventId?, transactionId: TransactionId?, @@ -293,21 +313,24 @@ class RustTimeline( mentions: List, ): Result = withContext(dispatcher) { - if (originalEventId != null) { - runCatching { - val editedEvent = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(originalEventId.value) - editedEvent.use { - inner.edit( - newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), - editItem = it, - ) + runCatching { + when { + originalEventId != null -> { + val editedEvent = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(originalEventId.value) + editedEvent.use { + inner.edit( + newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), + editItem = it, + ) + } + specialModeEventTimelineItem = null + } + transactionId != null -> { + error("Editing local echo is not supported yet.") + } + else -> { + error("Either originalEventId or transactionId must be non null") } - specialModeEventTimelineItem = null - } - } else { - runCatching { - transactionId?.let { cancelSend(it) } - inner.send(messageEventContentFromParts(body, htmlBody)) } } } @@ -426,17 +449,7 @@ class RustTimeline( } } - override suspend fun retrySendMessage(transactionId: TransactionId): Result = withContext(dispatcher) { - runCatching { - inner.retrySend(transactionId.value) - } - } - - override suspend fun cancelSend(transactionId: TransactionId): Result = withContext(dispatcher) { - runCatching { - inner.cancelSend(transactionId.value) - } - } + override suspend fun cancelSend(transactionId: TransactionId): Result = redactEvent(eventId = null, transactionId = transactionId, reason = null) override suspend fun sendLocation( body: String, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index ed56d4a8d0..a603d354e5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -79,7 +79,6 @@ fun RustEventSendState?.map(): LocalEventSendState? { RustEventSendState.NotSentYet -> LocalEventSendState.NotSentYet is RustEventSendState.SendingFailed -> LocalEventSendState.SendingFailed(error) is RustEventSendState.Sent -> LocalEventSendState.Sent(EventId(eventId)) - RustEventSendState.Cancelled -> LocalEventSendState.Canceled } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index be029705fb..0f7fae3a91 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryS import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.simulateLongTask import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -298,4 +299,13 @@ class FakeMatrixClient( } override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId) + + var setSendingQueueEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean -> + // no-op + } + + override suspend fun setSendingQueueEnabled(enabled: Boolean) = setSendingQueueEnabledLambda(enabled) + + var sendingQueueStatusFlow = MutableStateFlow(true) + override fun sendingQueueStatus(): StateFlow = sendingQueueStatusFlow } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index f220144485..281c763c03 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -113,7 +113,7 @@ class FakeMatrixRoom( private var updateUserRoleResult = Result.success(Unit) private var toggleReactionResult = Result.success(Unit) private var retrySendMessageResult = Result.success(Unit) - private var cancelSendResult = Result.success(Unit) + private var cancelSendResult = Result.success(true) private var forwardEventResult = Result.success(Unit) private var reportContentResult = Result.success(Unit) private var kickUserResult = Result.success(Unit) @@ -282,7 +282,7 @@ class FakeMatrixRoom( return retrySendMessageResult } - override suspend fun cancelSend(transactionId: TransactionId): Result { + override suspend fun cancelSend(transactionId: TransactionId): Result { cancelSendCount++ return cancelSendResult } @@ -295,14 +295,6 @@ class FakeMatrixRoom( return eventPermalinkResult(eventId) } - var redactEventEventIdParam: EventId? = null - private set - - override suspend fun redactEvent(eventId: EventId, reason: String?): Result { - redactEventEventIdParam = eventId - return Result.success(Unit) - } - override suspend fun leave(): Result { return leaveRoomLambda() } @@ -631,7 +623,7 @@ class FakeMatrixRoom( retrySendMessageResult = result } - fun givenCancelSendResult(result: Result) { + fun givenCancelSendResult(result: Result) { cancelSendResult = result } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index 1cb4a3c3eb..9f62df2ca6 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -69,6 +69,16 @@ class FakeTimeline( mentions: List, ): Result = sendMessageLambda(body, htmlBody, mentions) + var redactEventLambda: (eventId: EventId?, transactionId: TransactionId?, reason: String?) -> Result = { _, _, _ -> + Result.success(true) + } + + override suspend fun redactEvent( + eventId: EventId?, + transactionId: TransactionId?, + reason: String? + ): Result = redactEventLambda(eventId, transactionId, reason) + var editMessageLambda: ( originalEventId: EventId?, transactionId: TransactionId?, @@ -219,11 +229,7 @@ class FakeTimeline( var forwardEventLambda: (eventId: EventId, roomIds: List) -> Result = { _, _ -> Result.success(Unit) } override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = forwardEventLambda(eventId, roomIds) - var retrySendMessageLambda: (transactionId: TransactionId) -> Result = { Result.success(Unit) } - override suspend fun retrySendMessage(transactionId: TransactionId): Result = retrySendMessageLambda(transactionId) - - var cancelSendLambda: (transactionId: TransactionId) -> Result = { Result.success(Unit) } - override suspend fun cancelSend(transactionId: TransactionId): Result = cancelSendLambda(transactionId) + override suspend fun cancelSend(transactionId: TransactionId): Result = redactEvent(null, transactionId, null) var sendLocationLambda: ( body: String,