Merge pull request #3011 from element-hq/feature/fga/message_queuing
Feature : First iteration of message queuing
This commit is contained in:
commit
c091813d8c
37 changed files with 282 additions and 750 deletions
|
|
@ -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<LoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -157,6 +159,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
}
|
||||
)
|
||||
observeSyncStateAndNetworkStatus()
|
||||
setupSendingQueue()
|
||||
}
|
||||
|
||||
private fun setupSendingQueue() {
|
||||
sendingQueue.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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 = { _, _ -> },
|
||||
|
|
|
|||
|
|
@ -45,6 +45,5 @@ internal fun ATimelineItemEventRow(
|
|||
onMoreReactionsClick = {},
|
||||
onReadReceiptClick = {},
|
||||
onSwipeToReply = {},
|
||||
onTimestampClick = {},
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<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.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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<RetrySendMenuState> {
|
||||
override val values: Sequence<RetrySendMenuState> = sequenceOf(
|
||||
aRetrySendMenuState(),
|
||||
aRetrySendMenuState(event = aTimelineItemEvent()),
|
||||
)
|
||||
}
|
||||
|
||||
fun aRetrySendMenuState(
|
||||
event: TimelineItem.Event? = null,
|
||||
eventSink: (RetrySendMenuEvents) -> Unit = {},
|
||||
) = RetrySendMenuState(
|
||||
selectedEvent = event,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
|
|||
onLinkClick = onLinkClick,
|
||||
onMessageClick = onMessageClick,
|
||||
onMessageLongClick = onMessageLongClick,
|
||||
onTimestampClick = onTimestampClick,
|
||||
onSwipeToReply = onSwipeToReply,
|
||||
onReactionClick = onReactionClick,
|
||||
onReactionLongClick = onReactionLongClick,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `dismiss the bottom sheet emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
|
||||
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<RetrySendMenuEvents>()
|
||||
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<RetrySendMenuEvents>()
|
||||
rule.setRetrySendMessageMenu(
|
||||
aRetrySendMenuState(
|
||||
event = aTimelineItemEvent(),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_remove)
|
||||
eventsRecorder.assertSingle(RetrySendMenuEvents.Remove)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRetrySendMessageMenu(
|
||||
state: RetrySendMenuState,
|
||||
) {
|
||||
setContent {
|
||||
RetrySendMessageMenu(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -68,8 +68,12 @@ class PollRepository @Inject constructor(
|
|||
|
||||
suspend fun deletePoll(
|
||||
pollStartId: EventId,
|
||||
): Result<Unit> =
|
||||
room.redactEvent(
|
||||
eventId = pollStartId,
|
||||
)
|
||||
): Result<Boolean> =
|
||||
timelineProvider
|
||||
.getActiveTimeline()
|
||||
.redactEvent(
|
||||
eventId = pollStartId,
|
||||
transactionId = null,
|
||||
reason = null,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,4 +105,20 @@ interface MatrixClient : Closeable {
|
|||
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
|
||||
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<ResolvedRoomAlias>
|
||||
suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List<String>): Result<RoomPreview>
|
||||
|
||||
/**
|
||||
* 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<Boolean>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,8 +130,6 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
|
||||
|
||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
||||
|
||||
suspend fun sendImage(
|
||||
file: File,
|
||||
thumbnailFile: File?,
|
||||
|
|
@ -160,7 +158,7 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit>
|
||||
|
||||
suspend fun cancelSend(transactionId: TransactionId): Result<Unit>
|
||||
suspend fun cancelSend(transactionId: TransactionId): Result<Boolean>
|
||||
|
||||
suspend fun leave(): Result<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ interface Timeline : AutoCloseable {
|
|||
progressCallback: ProgressCallback?
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result<Boolean>
|
||||
|
||||
suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
|
||||
|
|
@ -91,9 +93,7 @@ interface Timeline : AutoCloseable {
|
|||
|
||||
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
|
||||
|
||||
suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit>
|
||||
|
||||
suspend fun cancelSend(transactionId: TransactionId): Result<Unit>
|
||||
suspend fun cancelSend(transactionId: TransactionId): Result<Boolean>
|
||||
|
||||
/**
|
||||
* Share a location message in the room.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Boolean> = 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) {
|
||||
|
|
|
|||
|
|
@ -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<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.leave()
|
||||
|
|
@ -435,10 +429,10 @@ class RustMatrixRoom(
|
|||
}
|
||||
|
||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> {
|
||||
return liveTimeline.retrySendMessage(transactionId)
|
||||
return Result.failure(UnsupportedOperationException("Not supported"))
|
||||
}
|
||||
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> {
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> {
|
||||
return liveTimeline.cancelSend(transactionId)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -279,12 +279,32 @@ class RustTimeline(
|
|||
|
||||
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(dispatcher) {
|
||||
messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content ->
|
||||
runCatching {
|
||||
runCatching<Unit> {
|
||||
inner.send(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result<Boolean> = 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<Mention>,
|
||||
): Result<Unit> =
|
||||
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<Unit> {
|
||||
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<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
inner.retrySend(transactionId.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
inner.cancelSend(transactionId.value)
|
||||
}
|
||||
}
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> = redactEvent(eventId = null, transactionId = transactionId, reason = null)
|
||||
|
||||
override suspend fun sendLocation(
|
||||
body: String,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Boolean> = sendingQueueStatusFlow
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Unit> {
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> {
|
||||
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<Unit> {
|
||||
redactEventEventIdParam = eventId
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun leave(): Result<Unit> {
|
||||
return leaveRoomLambda()
|
||||
}
|
||||
|
|
@ -631,7 +623,7 @@ class FakeMatrixRoom(
|
|||
retrySendMessageResult = result
|
||||
}
|
||||
|
||||
fun givenCancelSendResult(result: Result<Unit>) {
|
||||
fun givenCancelSendResult(result: Result<Boolean>) {
|
||||
cancelSendResult = result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,16 @@ class FakeTimeline(
|
|||
mentions: List<Mention>,
|
||||
): Result<Unit> = sendMessageLambda(body, htmlBody, mentions)
|
||||
|
||||
var redactEventLambda: (eventId: EventId?, transactionId: TransactionId?, reason: String?) -> Result<Boolean> = { _, _, _ ->
|
||||
Result.success(true)
|
||||
}
|
||||
|
||||
override suspend fun redactEvent(
|
||||
eventId: EventId?,
|
||||
transactionId: TransactionId?,
|
||||
reason: String?
|
||||
): Result<Boolean> = redactEventLambda(eventId, transactionId, reason)
|
||||
|
||||
var editMessageLambda: (
|
||||
originalEventId: EventId?,
|
||||
transactionId: TransactionId?,
|
||||
|
|
@ -219,11 +229,7 @@ class FakeTimeline(
|
|||
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
|
||||
|
||||
var retrySendMessageLambda: (transactionId: TransactionId) -> Result<Unit> = { Result.success(Unit) }
|
||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = retrySendMessageLambda(transactionId)
|
||||
|
||||
var cancelSendLambda: (transactionId: TransactionId) -> Result<Unit> = { Result.success(Unit) }
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = cancelSendLambda(transactionId)
|
||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> = redactEvent(null, transactionId, null)
|
||||
|
||||
var sendLocationLambda: (
|
||||
body: String,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue