Skip to content

Commit c091813

Browse files
authored
Merge pull request #3011 from element-hq/feature/fga/message_queuing
Feature : First iteration of message queuing
2 parents 756905b + 75c4f4f commit c091813

File tree

37 files changed

+282
-750
lines changed

37 files changed

+282
-750
lines changed

appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import dagger.assisted.AssistedInject
4141
import im.vector.app.features.analytics.plan.JoinedRoom
4242
import io.element.android.anvilannotations.ContributesNode
4343
import io.element.android.appnav.loggedin.LoggedInNode
44+
import io.element.android.appnav.loggedin.SendingQueue
4445
import io.element.android.appnav.room.RoomFlowNode
4546
import io.element.android.appnav.room.RoomNavigationTarget
4647
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
@@ -102,6 +103,7 @@ class LoggedInFlowNode @AssistedInject constructor(
102103
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
103104
private val shareEntryPoint: ShareEntryPoint,
104105
private val matrixClient: MatrixClient,
106+
private val sendingQueue: SendingQueue,
105107
snackbarDispatcher: SnackbarDispatcher,
106108
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
107109
backstack = BackStack(
@@ -157,6 +159,11 @@ class LoggedInFlowNode @AssistedInject constructor(
157159
}
158160
)
159161
observeSyncStateAndNetworkStatus()
162+
setupSendingQueue()
163+
}
164+
165+
private fun setupSendingQueue() {
166+
sendingQueue.launchIn(lifecycleScope)
160167
}
161168

162169
@OptIn(FlowPreview::class)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.appnav.loggedin
18+
19+
import androidx.annotation.VisibleForTesting
20+
import io.element.android.features.networkmonitor.api.NetworkMonitor
21+
import io.element.android.features.networkmonitor.api.NetworkStatus
22+
import io.element.android.libraries.di.SessionScope
23+
import io.element.android.libraries.di.SingleIn
24+
import io.element.android.libraries.matrix.api.MatrixClient
25+
import kotlinx.coroutines.CoroutineScope
26+
import kotlinx.coroutines.delay
27+
import kotlinx.coroutines.flow.combine
28+
import kotlinx.coroutines.flow.launchIn
29+
import kotlinx.coroutines.flow.onEach
30+
import timber.log.Timber
31+
import java.util.concurrent.atomic.AtomicInteger
32+
import javax.inject.Inject
33+
34+
private const val SENDING_QUEUE_MIN_RETRY_DELAY = 250L
35+
36+
@VisibleForTesting
37+
const val SENDING_QUEUE_MAX_RETRY_DELAY = 5000L
38+
39+
@SingleIn(SessionScope::class)
40+
class SendingQueue @Inject constructor(
41+
private val matrixClient: MatrixClient,
42+
private val networkMonitor: NetworkMonitor,
43+
) {
44+
private val retryCount = AtomicInteger(0)
45+
46+
fun launchIn(coroutineScope: CoroutineScope) {
47+
combine(
48+
networkMonitor.connectivity,
49+
matrixClient.sendingQueueStatus(),
50+
) { networkStatus, isSendingQueueEnabled ->
51+
Pair(networkStatus, isSendingQueueEnabled)
52+
}.onEach { (networkStatus, isSendingQueueEnabled) ->
53+
Timber.d("Network status: $networkStatus, isSendingQueueEnabled: $isSendingQueueEnabled")
54+
if (networkStatus == NetworkStatus.Online && !isSendingQueueEnabled) {
55+
val retryDelay =
56+
(SENDING_QUEUE_MIN_RETRY_DELAY * retryCount.incrementAndGet()).coerceIn(SENDING_QUEUE_MIN_RETRY_DELAY, SENDING_QUEUE_MAX_RETRY_DELAY)
57+
Timber.d("Retry enabling sending queue in $retryDelay ms")
58+
delay(retryDelay)
59+
} else {
60+
retryCount.set(0)
61+
}
62+
matrixClient.setSendingQueueEnabled(enabled = networkStatus == NetworkStatus.Online)
63+
}.launchIn(coroutineScope)
64+
}
65+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.appnav.loggedin
18+
19+
import io.element.android.features.networkmonitor.api.NetworkStatus
20+
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
21+
import io.element.android.libraries.matrix.test.FakeMatrixClient
22+
import io.element.android.tests.testutils.lambda.assert
23+
import io.element.android.tests.testutils.lambda.lambdaRecorder
24+
import io.element.android.tests.testutils.lambda.value
25+
import kotlinx.coroutines.ExperimentalCoroutinesApi
26+
import kotlinx.coroutines.flow.MutableStateFlow
27+
import kotlinx.coroutines.test.advanceTimeBy
28+
import kotlinx.coroutines.test.runTest
29+
import org.junit.Test
30+
31+
@OptIn(ExperimentalCoroutinesApi::class) class SendingQueueTest {
32+
private val matrixClient = FakeMatrixClient()
33+
private val networkMonitor = FakeNetworkMonitor()
34+
private val sut = SendingQueue(matrixClient, networkMonitor)
35+
36+
@Test
37+
fun `test network status online and sending queue is disabled`() = runTest {
38+
val sendingQueueStatusFlow = MutableStateFlow(false)
39+
val setEnableSendingQueueLambda = lambdaRecorder { _: Boolean -> }
40+
matrixClient.sendingQueueStatusFlow = sendingQueueStatusFlow
41+
matrixClient.setSendingQueueEnabledLambda = setEnableSendingQueueLambda
42+
43+
sut.launchIn(backgroundScope)
44+
45+
advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY)
46+
sendingQueueStatusFlow.value = true
47+
advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY)
48+
49+
assert(setEnableSendingQueueLambda)
50+
.isCalledExactly(2)
51+
.withSequence(
52+
listOf(value(true)),
53+
listOf(value(true))
54+
)
55+
}
56+
57+
@Test
58+
fun `test network status getting offline and online`() = runTest {
59+
val sendingQueueStatusFlow = MutableStateFlow(true)
60+
val setEnableSendingQueueLambda = lambdaRecorder { _: Boolean -> }
61+
matrixClient.sendingQueueStatusFlow = sendingQueueStatusFlow
62+
matrixClient.setSendingQueueEnabledLambda = setEnableSendingQueueLambda
63+
64+
sut.launchIn(backgroundScope)
65+
advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY)
66+
networkMonitor.connectivity.value = NetworkStatus.Offline
67+
advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY)
68+
networkMonitor.connectivity.value = NetworkStatus.Online
69+
advanceTimeBy(SENDING_QUEUE_MAX_RETRY_DELAY)
70+
71+
assert(setEnableSendingQueueLambda)
72+
.isCalledExactly(3)
73+
.withSequence(
74+
listOf(value(true)),
75+
listOf(value(false)),
76+
listOf(value(true)),
77+
)
78+
}
79+
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState
4646
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
4747
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
4848
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter
49-
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
5049
import io.element.android.features.messages.impl.timeline.model.TimelineItem
5150
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
5251
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
@@ -107,7 +106,6 @@ class MessagesPresenter @AssistedInject constructor(
107106
private val actionListPresenter: ActionListPresenter,
108107
private val customReactionPresenter: CustomReactionPresenter,
109108
private val reactionSummaryPresenter: ReactionSummaryPresenter,
110-
private val retrySendMenuPresenter: RetrySendMenuPresenter,
111109
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
112110
private val networkMonitor: NetworkMonitor,
113111
private val snackbarDispatcher: SnackbarDispatcher,
@@ -140,7 +138,6 @@ class MessagesPresenter @AssistedInject constructor(
140138
val actionListState = actionListPresenter.present()
141139
val customReactionState = customReactionPresenter.present()
142140
val reactionSummaryState = reactionSummaryPresenter.present()
143-
val retryState = retrySendMenuPresenter.present()
144141
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
145142

146143
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
@@ -231,7 +228,6 @@ class MessagesPresenter @AssistedInject constructor(
231228
actionListState = actionListState,
232229
customReactionState = customReactionState,
233230
reactionSummaryState = reactionSummaryState,
234-
retrySendMenuState = retryState,
235231
readReceiptBottomSheetState = readReceiptBottomSheetState,
236232
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
237233
snackbarMessage = snackbarMessage,
@@ -309,11 +305,9 @@ class MessagesPresenter @AssistedInject constructor(
309305
}
310306

311307
private suspend fun handleActionRedact(event: TimelineItem.Event) {
312-
if (event.failedToSend) {
313-
// If the message hasn't been sent yet, just cancel it
314-
event.transactionId?.let { room.cancelSend(it) }
315-
} else if (event.eventId != null) {
316-
room.redactEvent(event.eventId)
308+
timelineController.invokeOnCurrentTimeline {
309+
redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null)
310+
.onFailure { Timber.e(it) }
317311
}
318312
}
319313

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState
2323
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
2424
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
2525
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
26-
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
2726
import io.element.android.features.messages.impl.typing.TypingNotificationState
2827
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
2928
import io.element.android.libraries.architecture.AsyncData
@@ -47,7 +46,6 @@ data class MessagesState(
4746
val actionListState: ActionListState,
4847
val customReactionState: CustomReactionState,
4948
val reactionSummaryState: ReactionSummaryState,
50-
val retrySendMenuState: RetrySendMenuState,
5149
val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
5250
val hasNetworkConnection: Boolean,
5351
val snackbarMessage: SnackbarMessage?,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
3131
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
3232
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
3333
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
34-
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
35-
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.aRetrySendMenuState
3634
import io.element.android.features.messages.impl.timeline.model.TimelineItem
3735
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
3836
import io.element.android.features.messages.impl.typing.aTypingNotificationState
@@ -110,7 +108,6 @@ fun aMessagesState(
110108
// Render a focused event for an event with sender information displayed
111109
focusedEventIndex = 2,
112110
),
113-
retrySendMenuState: RetrySendMenuState = aRetrySendMenuState(),
114111
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
115112
actionListState: ActionListState = anActionListState(),
116113
customReactionState: CustomReactionState = aCustomReactionState(),
@@ -132,7 +129,6 @@ fun aMessagesState(
132129
voiceMessageComposerState = voiceMessageComposerState,
133130
typingNotificationState = aTypingNotificationState(),
134131
timelineState = timelineState,
135-
retrySendMenuState = retrySendMenuState,
136132
readReceiptBottomSheetState = readReceiptBottomSheetState,
137133
actionListState = actionListState,
138134
customReactionState = customReactionState,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
7474
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
7575
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet
7676
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
77-
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
78-
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
7977
import io.element.android.features.messages.impl.timeline.model.TimelineItem
8078
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
8179
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
@@ -103,7 +101,6 @@ import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
103101
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
104102
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
105103
import io.element.android.libraries.matrix.api.core.UserId
106-
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
107104
import io.element.android.libraries.ui.strings.CommonStrings
108105
import kotlinx.collections.immutable.ImmutableList
109106
import timber.log.Timber
@@ -212,11 +209,6 @@ fun MessagesView(
212209
onMessageLongClick = ::onMessageLongClick,
213210
onUserDataClick = onUserDataClick,
214211
onLinkClick = onLinkClick,
215-
onTimestampClick = { event ->
216-
if (event.localSendState is LocalEventSendState.SendingFailed) {
217-
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
218-
}
219-
},
220212
onReactionClick = ::onEmojiReactionClick,
221213
onReactionLongClick = ::onEmojiReactionLongClick,
222214
onMoreReactionsClick = ::onMoreReactionsClick,
@@ -258,7 +250,6 @@ fun MessagesView(
258250
)
259251

260252
ReactionSummaryView(state = state.reactionSummaryState)
261-
RetrySendMessageMenu(state = state.retrySendMenuState)
262253
ReadReceiptBottomSheet(
263254
state = state.readReceiptBottomSheetState,
264255
onUserDataClick = onUserDataClick,
@@ -319,7 +310,6 @@ private fun MessagesViewContent(
319310
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
320311
onReadReceiptClick: (TimelineItem.Event) -> Unit,
321312
onMessageLongClick: (TimelineItem.Event) -> Unit,
322-
onTimestampClick: (TimelineItem.Event) -> Unit,
323313
onSendLocationClick: () -> Unit,
324314
onCreatePollClick: () -> Unit,
325315
onJoinCallClick: () -> Unit,
@@ -387,7 +377,6 @@ private fun MessagesViewContent(
387377
onLinkClick = onLinkClick,
388378
onMessageClick = onMessageClick,
389379
onMessageLongClick = onMessageLongClick,
390-
onTimestampClick = onTimestampClick,
391380
onSwipeToReply = onSwipeToReply,
392381
onReactionClick = onReactionClick,
393382
onReactionLongClick = onReactionLongClick,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ class ActionListPresenter @Inject constructor(
181181
add(TimelineItemAction.Forward)
182182
}
183183
}
184-
if (timelineItem.isMine && timelineItem.isTextMessage) {
184+
if (timelineItem.isEditable) {
185185
add(TimelineItemAction.Edit)
186186
}
187187
if (timelineItem.content.canBeCopied()) {

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ fun TimelineView(
8383
onLinkClick: (String) -> Unit,
8484
onMessageClick: (TimelineItem.Event) -> Unit,
8585
onMessageLongClick: (TimelineItem.Event) -> Unit,
86-
onTimestampClick: (TimelineItem.Event) -> Unit,
8786
onSwipeToReply: (TimelineItem.Event) -> Unit,
8887
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit,
8988
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
@@ -148,7 +147,6 @@ fun TimelineView(
148147
onReactionLongClick = onReactionLongClick,
149148
onMoreReactionsClick = onMoreReactionsClick,
150149
onReadReceiptClick = onReadReceiptClick,
151-
onTimestampClick = onTimestampClick,
152150
eventSink = state.eventSink,
153151
onSwipeToReply = onSwipeToReply,
154152
onJoinCallClick = onJoinCallClick,
@@ -245,8 +243,8 @@ private fun BoxScope.TimelineScrollHelper(
245243
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
246244
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
247245
modifier = Modifier
248-
.align(Alignment.BottomEnd)
249-
.padding(end = 24.dp, bottom = 12.dp),
246+
.align(Alignment.BottomEnd)
247+
.padding(end = 24.dp, bottom = 12.dp),
250248
onClick = { jumpToBottom() },
251249
)
252250
}
@@ -273,8 +271,8 @@ private fun JumpToBottomButton(
273271
) {
274272
Icon(
275273
modifier = Modifier
276-
.size(24.dp)
277-
.rotate(90f),
274+
.size(24.dp)
275+
.rotate(90f),
278276
imageVector = CompoundIcons.ArrowRight(),
279277
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
280278
)
@@ -301,7 +299,6 @@ internal fun TimelineViewPreview(
301299
onLinkClick = {},
302300
onMessageClick = {},
303301
onMessageLongClick = {},
304-
onTimestampClick = {},
305302
onSwipeToReply = {},
306303
onReactionClick = { _, _ -> },
307304
onReactionLongClick = { _, _ -> },

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,5 @@ internal fun ATimelineItemEventRow(
4545
onMoreReactionsClick = {},
4646
onReadReceiptClick = {},
4747
onSwipeToReply = {},
48-
onTimestampClick = {},
4948
eventSink = {},
5049
)

0 commit comments

Comments
 (0)