Merge pull request #3011 from element-hq/feature/fga/message_queuing
Feature : First iteration of message queuing
This commit is contained in:
commit
9776520d7d
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 im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.loggedin.LoggedInNode
|
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.RoomFlowNode
|
||||||
import io.element.android.appnav.room.RoomNavigationTarget
|
import io.element.android.appnav.room.RoomNavigationTarget
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
|
|
@ -102,6 +103,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
|
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
|
||||||
private val shareEntryPoint: ShareEntryPoint,
|
private val shareEntryPoint: ShareEntryPoint,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
|
private val sendingQueue: SendingQueue,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -157,6 +159,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
observeSyncStateAndNetworkStatus()
|
observeSyncStateAndNetworkStatus()
|
||||||
|
setupSendingQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSendingQueue() {
|
||||||
|
sendingQueue.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@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.customreaction.CustomReactionPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
|
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.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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
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 actionListPresenter: ActionListPresenter,
|
||||||
private val customReactionPresenter: CustomReactionPresenter,
|
private val customReactionPresenter: CustomReactionPresenter,
|
||||||
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
||||||
private val retrySendMenuPresenter: RetrySendMenuPresenter,
|
|
||||||
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
|
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
|
@ -140,7 +138,6 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
val actionListState = actionListPresenter.present()
|
val actionListState = actionListPresenter.present()
|
||||||
val customReactionState = customReactionPresenter.present()
|
val customReactionState = customReactionPresenter.present()
|
||||||
val reactionSummaryState = reactionSummaryPresenter.present()
|
val reactionSummaryState = reactionSummaryPresenter.present()
|
||||||
val retryState = retrySendMenuPresenter.present()
|
|
||||||
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
|
|
@ -231,7 +228,6 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
actionListState = actionListState,
|
actionListState = actionListState,
|
||||||
customReactionState = customReactionState,
|
customReactionState = customReactionState,
|
||||||
reactionSummaryState = reactionSummaryState,
|
reactionSummaryState = reactionSummaryState,
|
||||||
retrySendMenuState = retryState,
|
|
||||||
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
||||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
|
|
@ -309,11 +305,9 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
||||||
if (event.failedToSend) {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
// If the message hasn't been sent yet, just cancel it
|
redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null)
|
||||||
event.transactionId?.let { room.cancelSend(it) }
|
.onFailure { Timber.e(it) }
|
||||||
} else if (event.eventId != null) {
|
|
||||||
room.redactEvent(event.eventId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.customreaction.CustomReactionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
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.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.typing.TypingNotificationState
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
@ -47,7 +46,6 @@ data class MessagesState(
|
||||||
val actionListState: ActionListState,
|
val actionListState: ActionListState,
|
||||||
val customReactionState: CustomReactionState,
|
val customReactionState: CustomReactionState,
|
||||||
val reactionSummaryState: ReactionSummaryState,
|
val reactionSummaryState: ReactionSummaryState,
|
||||||
val retrySendMenuState: RetrySendMenuState,
|
|
||||||
val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
|
val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
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.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.ReadReceiptBottomSheetEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
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
|
// Render a focused event for an event with sender information displayed
|
||||||
focusedEventIndex = 2,
|
focusedEventIndex = 2,
|
||||||
),
|
),
|
||||||
retrySendMenuState: RetrySendMenuState = aRetrySendMenuState(),
|
|
||||||
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
|
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
|
||||||
actionListState: ActionListState = anActionListState(),
|
actionListState: ActionListState = anActionListState(),
|
||||||
customReactionState: CustomReactionState = aCustomReactionState(),
|
customReactionState: CustomReactionState = aCustomReactionState(),
|
||||||
|
|
@ -132,7 +129,6 @@ fun aMessagesState(
|
||||||
voiceMessageComposerState = voiceMessageComposerState,
|
voiceMessageComposerState = voiceMessageComposerState,
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
retrySendMenuState = retrySendMenuState,
|
|
||||||
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
||||||
actionListState = actionListState,
|
actionListState = actionListState,
|
||||||
customReactionState = customReactionState,
|
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.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.ReadReceiptBottomSheet
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
|
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.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
|
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.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -212,11 +209,6 @@ fun MessagesView(
|
||||||
onMessageLongClick = ::onMessageLongClick,
|
onMessageLongClick = ::onMessageLongClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onTimestampClick = { event ->
|
|
||||||
if (event.localSendState is LocalEventSendState.SendingFailed) {
|
|
||||||
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onReactionClick = ::onEmojiReactionClick,
|
onReactionClick = ::onEmojiReactionClick,
|
||||||
onReactionLongClick = ::onEmojiReactionLongClick,
|
onReactionLongClick = ::onEmojiReactionLongClick,
|
||||||
onMoreReactionsClick = ::onMoreReactionsClick,
|
onMoreReactionsClick = ::onMoreReactionsClick,
|
||||||
|
|
@ -258,7 +250,6 @@ fun MessagesView(
|
||||||
)
|
)
|
||||||
|
|
||||||
ReactionSummaryView(state = state.reactionSummaryState)
|
ReactionSummaryView(state = state.reactionSummaryState)
|
||||||
RetrySendMessageMenu(state = state.retrySendMenuState)
|
|
||||||
ReadReceiptBottomSheet(
|
ReadReceiptBottomSheet(
|
||||||
state = state.readReceiptBottomSheetState,
|
state = state.readReceiptBottomSheetState,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
|
|
@ -319,7 +310,6 @@ private fun MessagesViewContent(
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
onMessageLongClick: (TimelineItem.Event) -> Unit,
|
onMessageLongClick: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
|
||||||
onSendLocationClick: () -> Unit,
|
onSendLocationClick: () -> Unit,
|
||||||
onCreatePollClick: () -> Unit,
|
onCreatePollClick: () -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
|
|
@ -387,7 +377,6 @@ private fun MessagesViewContent(
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onMessageClick = onMessageClick,
|
onMessageClick = onMessageClick,
|
||||||
onMessageLongClick = onMessageLongClick,
|
onMessageLongClick = onMessageLongClick,
|
||||||
onTimestampClick = onTimestampClick,
|
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
add(TimelineItemAction.Forward)
|
add(TimelineItemAction.Forward)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (timelineItem.isMine && timelineItem.isTextMessage) {
|
if (timelineItem.isEditable) {
|
||||||
add(TimelineItemAction.Edit)
|
add(TimelineItemAction.Edit)
|
||||||
}
|
}
|
||||||
if (timelineItem.content.canBeCopied()) {
|
if (timelineItem.content.canBeCopied()) {
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ fun TimelineView(
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
onMessageClick: (TimelineItem.Event) -> Unit,
|
onMessageClick: (TimelineItem.Event) -> Unit,
|
||||||
onMessageLongClick: (TimelineItem.Event) -> Unit,
|
onMessageLongClick: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit,
|
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -148,7 +147,6 @@ fun TimelineView(
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onTimestampClick = onTimestampClick,
|
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
onJoinCallClick = onJoinCallClick,
|
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
|
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
|
||||||
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(end = 24.dp, bottom = 12.dp),
|
.padding(end = 24.dp, bottom = 12.dp),
|
||||||
onClick = { jumpToBottom() },
|
onClick = { jumpToBottom() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -273,8 +271,8 @@ private fun JumpToBottomButton(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.rotate(90f),
|
.rotate(90f),
|
||||||
imageVector = CompoundIcons.ArrowRight(),
|
imageVector = CompoundIcons.ArrowRight(),
|
||||||
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
|
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
|
||||||
)
|
)
|
||||||
|
|
@ -301,7 +299,6 @@ internal fun TimelineViewPreview(
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
onMessageClick = {},
|
onMessageClick = {},
|
||||||
onMessageLongClick = {},
|
onMessageLongClick = {},
|
||||||
onTimestampClick = {},
|
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,5 @@ internal fun ATimelineItemEventRow(
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
onReadReceiptClick = {},
|
onReadReceiptClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onTimestampClick = {},
|
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,61 +16,34 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
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.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.isEdited
|
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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineEventTimestampView(
|
fun TimelineEventTimestampView(
|
||||||
event: TimelineItem.Event,
|
formattedTime: String,
|
||||||
onClick: () -> Unit,
|
isMessageEdited: Boolean,
|
||||||
onLongClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(clickModifier)
|
|
||||||
// Add extra padding for touch target size
|
|
||||||
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -79,35 +52,22 @@ fun TimelineEventTimestampView(
|
||||||
Text(
|
Text(
|
||||||
stringResource(CommonStrings.common_edited_suffix),
|
stringResource(CommonStrings.common_edited_suffix),
|
||||||
style = ElementTheme.typography.fontBodyXsRegular,
|
style = ElementTheme.typography.fontBodyXsRegular,
|
||||||
color = tint ?: MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
formattedTime,
|
formattedTime,
|
||||||
style = ElementTheme.typography.fontBodyXsRegular,
|
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
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
|
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(formattedTime = event.sentTime, isMessageEdited = event.content.isEdited())
|
||||||
event = event,
|
|
||||||
onClick = {},
|
|
||||||
onLongClick = {},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object TimelineEventTimestampViewDefaults {
|
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.aTimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
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.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.eventId
|
||||||
import io.element.android.features.messages.impl.timeline.model.metadata
|
import io.element.android.features.messages.impl.timeline.model.metadata
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||||
|
|
@ -130,7 +131,6 @@ fun TimelineItemEventRow(
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
|
||||||
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -190,7 +190,6 @@ fun TimelineItemEventRow(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onTimestampClick = onTimestampClick,
|
|
||||||
inReplyToClick = ::inReplyToClick,
|
inReplyToClick = ::inReplyToClick,
|
||||||
onUserDataClick = ::onUserDataClick,
|
onUserDataClick = ::onUserDataClick,
|
||||||
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
||||||
|
|
@ -209,7 +208,6 @@ fun TimelineItemEventRow(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onTimestampClick = onTimestampClick,
|
|
||||||
inReplyToClick = ::inReplyToClick,
|
inReplyToClick = ::inReplyToClick,
|
||||||
onUserDataClick = ::onUserDataClick,
|
onUserDataClick = ::onUserDataClick,
|
||||||
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
||||||
|
|
@ -265,7 +263,6 @@ private fun TimelineItemEventRowContent(
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
|
||||||
inReplyToClick: () -> Unit,
|
inReplyToClick: () -> Unit,
|
||||||
onUserDataClick: () -> Unit,
|
onUserDataClick: () -> Unit,
|
||||||
onReactionClick: (emoji: String) -> Unit,
|
onReactionClick: (emoji: String) -> Unit,
|
||||||
|
|
@ -337,9 +334,6 @@ private fun TimelineItemEventRowContent(
|
||||||
event = event,
|
event = event,
|
||||||
onMessageLongClick = onLongClick,
|
onMessageLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onTimestampClick = {
|
|
||||||
onTimestampClick(event)
|
|
||||||
},
|
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
@ -419,7 +413,6 @@ private fun MessageEventBubbleContent(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
onMessageLongClick: () -> Unit,
|
onMessageLongClick: () -> Unit,
|
||||||
inReplyToClick: () -> Unit,
|
inReplyToClick: () -> Unit,
|
||||||
onTimestampClick: () -> Unit,
|
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
@SuppressLint("ModifierParameter")
|
@SuppressLint("ModifierParameter")
|
||||||
|
|
@ -467,9 +460,8 @@ private fun MessageEventBubbleContent(
|
||||||
Box(modifier, contentAlignment = Alignment.Center) {
|
Box(modifier, contentAlignment = Alignment.Center) {
|
||||||
content {}
|
content {}
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
formattedTime = event.sentTime,
|
||||||
onClick = onTimestampClick,
|
isMessageEdited = event.content.isEdited(),
|
||||||
onLongClick = ::onTimestampLongClick,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// Outer padding
|
// Outer padding
|
||||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||||
|
|
@ -489,9 +481,8 @@ private fun MessageEventBubbleContent(
|
||||||
content = { content(this::onContentLayoutChange) },
|
content = { content(this::onContentLayoutChange) },
|
||||||
overlay = {
|
overlay = {
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
formattedTime = event.sentTime,
|
||||||
onClick = onTimestampClick,
|
isMessageEdited = event.content.isEdited(),
|
||||||
onLongClick = ::onTimestampLongClick,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -501,9 +492,8 @@ private fun MessageEventBubbleContent(
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
content {}
|
content {}
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
formattedTime = event.sentTime,
|
||||||
onClick = onTimestampClick,
|
isMessageEdited = event.content.isEdited(),
|
||||||
onLongClick = ::onTimestampLongClick,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ fun TimelineItemGroupedEventsRow(
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
|
||||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -76,7 +75,6 @@ fun TimelineItemGroupedEventsRow(
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onTimestampClick = onTimestampClick,
|
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
|
@ -100,7 +98,6 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
|
||||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -133,7 +130,6 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onTimestampClick = onTimestampClick,
|
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
|
@ -175,7 +171,6 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
onTimestampClick = {},
|
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
|
@ -200,7 +195,6 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
onTimestampClick = {},
|
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ internal fun TimelineItemRow(
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit,
|
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
|
|
@ -118,7 +117,6 @@ internal fun TimelineItemRow(
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onTimestampClick = onTimestampClick,
|
|
||||||
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
@ -137,7 +135,6 @@ internal fun TimelineItemRow(
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onTimestampClick = onTimestampClick,
|
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,8 @@ fun TimelineItemReadReceiptView(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
when (state.sendState) {
|
when (state.sendState) {
|
||||||
LocalEventSendState.NotSentYet -> {
|
is LocalEventSendState.SendingFailed,
|
||||||
|
is LocalEventSendState.NotSentYet -> {
|
||||||
ReadReceiptsRow(modifier) {
|
ReadReceiptsRow(modifier) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.padding(2.dp),
|
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,
|
null,
|
||||||
is LocalEventSendState.Sent -> {
|
is LocalEventSendState.Sent -> {
|
||||||
if (state.isLastOutgoingMessage) {
|
if (state.isLastOutgoingMessage) {
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,6 @@ private fun ReadReceiptBottomSheetContent(
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) = ElementPreview {
|
internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) = ElementPreview {
|
||||||
// TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed
|
|
||||||
Column {
|
Column {
|
||||||
ReadReceiptBottomSheetContent(
|
ReadReceiptBottomSheetContent(
|
||||||
state = state,
|
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.customreaction.FakeEmojibaseProvider
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
|
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.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.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
|
|
@ -839,7 +838,6 @@ class MessagesPresenterTest {
|
||||||
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
||||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||||
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
composerPresenter = messageComposerPresenter,
|
composerPresenter = messageComposerPresenter,
|
||||||
|
|
@ -849,7 +847,6 @@ class MessagesPresenterTest {
|
||||||
actionListPresenter = actionListPresenter,
|
actionListPresenter = actionListPresenter,
|
||||||
customReactionPresenter = customReactionPresenter,
|
customReactionPresenter = customReactionPresenter,
|
||||||
reactionSummaryPresenter = reactionSummaryPresenter,
|
reactionSummaryPresenter = reactionSummaryPresenter,
|
||||||
retrySendMenuPresenter = retrySendMenuPresenter,
|
|
||||||
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
|
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
|
||||||
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
|
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onMessageLongClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
onMessageLongClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onTimestampClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
onSwipeToReply: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
|
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
|
||||||
onReactionLongClick: (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,
|
onLinkClick = onLinkClick,
|
||||||
onMessageClick = onMessageClick,
|
onMessageClick = onMessageClick,
|
||||||
onMessageLongClick = onMessageLongClick,
|
onMessageLongClick = onMessageLongClick,
|
||||||
onTimestampClick = onTimestampClick,
|
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
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(
|
suspend fun deletePoll(
|
||||||
pollStartId: EventId,
|
pollStartId: EventId,
|
||||||
): Result<Unit> =
|
): Result<Boolean> =
|
||||||
room.redactEvent(
|
timelineProvider
|
||||||
eventId = pollStartId,
|
.getActiveTimeline()
|
||||||
)
|
.redactEvent(
|
||||||
|
eventId = pollStartId,
|
||||||
|
transactionId = null,
|
||||||
|
reason = null,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,4 +105,20 @@ interface MatrixClient : Closeable {
|
||||||
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
|
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
|
||||||
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<ResolvedRoomAlias>
|
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<ResolvedRoomAlias>
|
||||||
suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List<String>): Result<RoomPreview>
|
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 sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
|
||||||
|
|
||||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
|
||||||
|
|
||||||
suspend fun sendImage(
|
suspend fun sendImage(
|
||||||
file: File,
|
file: File,
|
||||||
thumbnailFile: File?,
|
thumbnailFile: File?,
|
||||||
|
|
@ -160,7 +158,7 @@ interface MatrixRoom : Closeable {
|
||||||
|
|
||||||
suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit>
|
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>
|
suspend fun leave(): Result<Unit>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ interface Timeline : AutoCloseable {
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<MediaUploadHandler>
|
): 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 sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
|
||||||
|
|
||||||
suspend fun sendFile(file: File, fileInfo: FileInfo, 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 forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
|
||||||
|
|
||||||
suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit>
|
suspend fun cancelSend(transactionId: TransactionId): Result<Boolean>
|
||||||
|
|
||||||
suspend fun cancelSend(transactionId: TransactionId): Result<Unit>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share a location message in the room.
|
* Share a location message in the room.
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface LocalEventSendState {
|
sealed interface LocalEventSendState {
|
||||||
data object NotSentYet : LocalEventSendState
|
data object NotSentYet : LocalEventSendState
|
||||||
data object Canceled : LocalEventSendState
|
|
||||||
|
|
||||||
data class SendingFailed(
|
data class SendingFailed(
|
||||||
val error: String
|
val error: String
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||||
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
||||||
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
||||||
import org.matrix.rustcomponents.sdk.PowerLevels
|
import org.matrix.rustcomponents.sdk.PowerLevels
|
||||||
|
import org.matrix.rustcomponents.sdk.SendingQueueStatusListener
|
||||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -551,6 +552,21 @@ class RustMatrixClient(
|
||||||
}.distinctUntilChanged()
|
}.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(
|
private suspend fun File.getCacheSize(
|
||||||
includeCryptoDb: Boolean = false,
|
includeCryptoDb: Boolean = false,
|
||||||
): Long = withContext(sessionDispatcher) {
|
): Long = withContext(sessionDispatcher) {
|
||||||
|
|
|
||||||
|
|
@ -324,12 +324,6 @@ class RustMatrixRoom(
|
||||||
return liveTimeline.sendMessage(body, htmlBody, mentions)
|
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) {
|
override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
innerRoom.leave()
|
innerRoom.leave()
|
||||||
|
|
@ -435,10 +429,10 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> {
|
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)
|
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) {
|
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(dispatcher) {
|
||||||
messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content ->
|
messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content ->
|
||||||
runCatching {
|
runCatching<Unit> {
|
||||||
inner.send(content)
|
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(
|
override suspend fun editMessage(
|
||||||
originalEventId: EventId?,
|
originalEventId: EventId?,
|
||||||
transactionId: TransactionId?,
|
transactionId: TransactionId?,
|
||||||
|
|
@ -293,21 +313,24 @@ class RustTimeline(
|
||||||
mentions: List<Mention>,
|
mentions: List<Mention>,
|
||||||
): Result<Unit> =
|
): Result<Unit> =
|
||||||
withContext(dispatcher) {
|
withContext(dispatcher) {
|
||||||
if (originalEventId != null) {
|
runCatching<Unit> {
|
||||||
runCatching {
|
when {
|
||||||
val editedEvent = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(originalEventId.value)
|
originalEventId != null -> {
|
||||||
editedEvent.use {
|
val editedEvent = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(originalEventId.value)
|
||||||
inner.edit(
|
editedEvent.use {
|
||||||
newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()),
|
inner.edit(
|
||||||
editItem = it,
|
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) {
|
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> = redactEvent(eventId = null, transactionId = transactionId, reason = null)
|
||||||
runCatching {
|
|
||||||
inner.retrySend(transactionId.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = withContext(dispatcher) {
|
|
||||||
runCatching {
|
|
||||||
inner.cancelSend(transactionId.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun sendLocation(
|
override suspend fun sendLocation(
|
||||||
body: String,
|
body: String,
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ fun RustEventSendState?.map(): LocalEventSendState? {
|
||||||
RustEventSendState.NotSentYet -> LocalEventSendState.NotSentYet
|
RustEventSendState.NotSentYet -> LocalEventSendState.NotSentYet
|
||||||
is RustEventSendState.SendingFailed -> LocalEventSendState.SendingFailed(error)
|
is RustEventSendState.SendingFailed -> LocalEventSendState.SendingFailed(error)
|
||||||
is RustEventSendState.Sent -> LocalEventSendState.Sent(EventId(eventId))
|
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.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
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 io.element.android.tests.testutils.simulateLongTask
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -298,4 +299,13 @@ class FakeMatrixClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId)
|
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 updateUserRoleResult = Result.success(Unit)
|
||||||
private var toggleReactionResult = Result.success(Unit)
|
private var toggleReactionResult = Result.success(Unit)
|
||||||
private var retrySendMessageResult = 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 forwardEventResult = Result.success(Unit)
|
||||||
private var reportContentResult = Result.success(Unit)
|
private var reportContentResult = Result.success(Unit)
|
||||||
private var kickUserResult = Result.success(Unit)
|
private var kickUserResult = Result.success(Unit)
|
||||||
|
|
@ -282,7 +282,7 @@ class FakeMatrixRoom(
|
||||||
return retrySendMessageResult
|
return retrySendMessageResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> {
|
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> {
|
||||||
cancelSendCount++
|
cancelSendCount++
|
||||||
return cancelSendResult
|
return cancelSendResult
|
||||||
}
|
}
|
||||||
|
|
@ -295,14 +295,6 @@ class FakeMatrixRoom(
|
||||||
return eventPermalinkResult(eventId)
|
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> {
|
override suspend fun leave(): Result<Unit> {
|
||||||
return leaveRoomLambda()
|
return leaveRoomLambda()
|
||||||
}
|
}
|
||||||
|
|
@ -631,7 +623,7 @@ class FakeMatrixRoom(
|
||||||
retrySendMessageResult = result
|
retrySendMessageResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun givenCancelSendResult(result: Result<Unit>) {
|
fun givenCancelSendResult(result: Result<Boolean>) {
|
||||||
cancelSendResult = result
|
cancelSendResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,16 @@ class FakeTimeline(
|
||||||
mentions: List<Mention>,
|
mentions: List<Mention>,
|
||||||
): Result<Unit> = sendMessageLambda(body, htmlBody, mentions)
|
): 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: (
|
var editMessageLambda: (
|
||||||
originalEventId: EventId?,
|
originalEventId: EventId?,
|
||||||
transactionId: TransactionId?,
|
transactionId: TransactionId?,
|
||||||
|
|
@ -219,11 +229,7 @@ class FakeTimeline(
|
||||||
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
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)
|
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 cancelSend(transactionId: TransactionId): Result<Boolean> = redactEvent(null, transactionId, null)
|
||||||
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)
|
|
||||||
|
|
||||||
var sendLocationLambda: (
|
var sendLocationLambda: (
|
||||||
body: String,
|
body: String,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue