diff --git a/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt index 95b8c43808..bbc3915c86 100644 --- a/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt +++ b/features/forward/api/src/main/kotlin/io/element/android/features/forward/api/ForwardEntryPoint.kt @@ -23,7 +23,7 @@ interface ForwardEntryPoint : FeatureEntryPoint { } data class Params( - val eventId: EventId, + val eventIds: List, val timelineProvider: TimelineProvider, ) : NodeInputs diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt index 97256c6576..e46e268a55 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPoint.kt @@ -27,7 +27,7 @@ class DefaultForwardEntryPoint : ForwardEntryPoint { buildContext = buildContext, plugins = listOf( ForwardMessagesNode.Inputs( - eventId = params.eventId, + eventIds = params.eventIds, timelineProvider = params.timelineProvider, ), callback, diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt index 1c565a390a..30a48c91de 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt @@ -52,13 +52,13 @@ class ForwardMessagesNode( object NavTarget : Parcelable data class Inputs( - val eventId: EventId, + val eventIds: List, val timelineProvider: TimelineProvider, ) : NodeInputs private val inputs = inputs() private val callback: ForwardEntryPoint.Callback = callback() - private val presenter = presenterFactory.create(inputs.eventId.value, inputs.timelineProvider) + private val presenter = presenterFactory.create(inputs.eventIds, inputs.timelineProvider) override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { val callback = object : RoomSelectEntryPoint.Callback { diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt index 6068866653..a36be20ab4 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt @@ -28,22 +28,20 @@ import timber.log.Timber @AssistedInject class ForwardMessagesPresenter( - @Assisted eventId: String, + @Assisted private val eventIds: List, @Assisted private val timelineProvider: TimelineProvider, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : Presenter { - private val eventId: EventId = EventId(eventId) - @AssistedFactory fun interface Factory { - fun create(eventId: String, timelineProvider: TimelineProvider): ForwardMessagesPresenter + fun create(eventIds: List, timelineProvider: TimelineProvider): ForwardMessagesPresenter } private val forwardingActionState: MutableState>> = mutableStateOf(AsyncAction.Uninitialized) fun onRoomSelected(roomIds: List) { - sessionCoroutineScope.forwardEvent(eventId, roomIds) + sessionCoroutineScope.forwardEvents(eventIds, roomIds) } @Composable @@ -60,14 +58,14 @@ class ForwardMessagesPresenter( ) } - private fun CoroutineScope.forwardEvent( - eventId: EventId, + private fun CoroutineScope.forwardEvents( + eventIds: List, roomIds: List, ) = launch { suspend { - timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds) + timelineProvider.getActiveTimeline().forwardEvents(eventIds, roomIds) .onFailure { - Timber.e(it, "Error while forwarding event") + Timber.e(it, "Error while forwarding events") } .getOrThrow() roomIds diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt index 9ee932a246..c082e4ecd3 100644 --- a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt +++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/DefaultForwardEntryPointTest.kt @@ -45,7 +45,7 @@ class DefaultForwardEntryPointTest { override fun onDone(roomIds: List) = lambdaError() } val params = ForwardEntryPoint.Params( - eventId = AN_EVENT_ID, + eventIds = listOf(AN_EVENT_ID), timelineProvider = FakeTimelineProvider(), ) val result = entryPoint.createNode( @@ -57,7 +57,7 @@ class DefaultForwardEntryPointTest { assertThat(result).isInstanceOf(ForwardMessagesNode::class.java) assertThat(result.plugins).contains( ForwardMessagesNode.Inputs( - eventId = params.eventId, + eventIds = params.eventIds, timelineProvider = params.timelineProvider, ) ) diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt index 0ff3210f2e..5fda2e252c 100644 --- a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt +++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt @@ -44,11 +44,11 @@ class ForwardMessagesPresenterTest { @Test fun `present - forward successful`() = runTest { - val forwardEventLambda = lambdaRecorder { _: EventId, _: List -> + val forwardEventsLambda = lambdaRecorder { _: List, _: List -> Result.success(Unit) } val timeline = FakeTimeline().apply { - this.forwardEventLambda = forwardEventLambda + this.forwardEventsLambda = forwardEventsLambda } val room = FakeJoinedRoom(liveTimeline = timeline) val presenter = createForwardMessagesPresenter(fakeRoom = room) @@ -62,17 +62,17 @@ class ForwardMessagesPresenterTest { assertThat(forwardingState.forwardAction.isLoading()).isTrue() val successfulForwardState = awaitItem() assertThat(successfulForwardState.forwardAction).isEqualTo(AsyncAction.Success(listOf(summary.roomId))) - forwardEventLambda.assertions().isCalledOnce() + forwardEventsLambda.assertions().isCalledOnce() } } @Test fun `present - select a room and forward failed, then clear`() = runTest { - val forwardEventLambda = lambdaRecorder { _: EventId, _: List -> + val forwardEventsLambda = lambdaRecorder { _: List, _: List -> Result.failure(IllegalStateException("error")) } val timeline = FakeTimeline().apply { - this.forwardEventLambda = forwardEventLambda + this.forwardEventsLambda = forwardEventsLambda } val room = FakeJoinedRoom(liveTimeline = timeline) val presenter = createForwardMessagesPresenter(fakeRoom = room) @@ -88,16 +88,16 @@ class ForwardMessagesPresenterTest { // Then clear error failedForwardState.eventSink(ForwardMessagesEvent.ClearError) assertThat(awaitItem().forwardAction.isUninitialized()).isTrue() - forwardEventLambda.assertions().isCalledOnce() + forwardEventsLambda.assertions().isCalledOnce() } } } fun TestScope.createForwardMessagesPresenter( - eventId: EventId = AN_EVENT_ID, + eventIds: List = listOf(AN_EVENT_ID), fakeRoom: FakeJoinedRoom = FakeJoinedRoom(), ) = ForwardMessagesPresenter( - eventId = eventId.value, + eventIds = eventIds, timelineProvider = LiveTimelineProvider(fakeRoom), sessionCoroutineScope = this, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 72483e68f1..51a8964aa0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -316,12 +316,13 @@ interface Timeline : AutoCloseable { suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result /** - * Forwards the content of an event to other rooms, as new messages sent by the current user. + * Forwards the content of some events to other rooms, as new messages sent by the current user. + * The events are forwarded in the order of [eventIds]. * - * @param eventId the event whose content is forwarded. - * @param roomIds the rooms to forward it to. + * @param eventIds the events whose content is forwarded. + * @param roomIds the rooms to forward them to. */ - suspend fun forwardEvent(eventId: EventId, roomIds: List): Result + suspend fun forwardEvents(eventIds: List, roomIds: List): Result /** * Cancels an event that is still waiting in the send queue, by redacting its local echo. diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt index 39cde68c31..8191082806 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt @@ -14,7 +14,6 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.ForwardEventException import io.element.android.libraries.matrix.impl.roomlist.roomOrNull -import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered import kotlinx.coroutines.withTimeout import org.matrix.rustcomponents.sdk.MsgLikeKind import org.matrix.rustcomponents.sdk.RoomListService @@ -31,33 +30,36 @@ class RoomContentForwarder( private val roomListService: RoomListService, ) { /** - * Forwards the event with the given [eventId] from the [fromTimeline] to the given [toRoomIds]. - * @param fromTimeline the room to forward the event from - * @param eventId the id of the event to forward - * @param toRoomIds the ids of the rooms to forward the event to - * @param timeoutMs the maximum time in milliseconds to wait for the event to be sent to a room + * Forwards the events with the given [eventIds] from the [fromTimeline] to the given [toRoomIds]. + * The events are sent to each room in the order they are provided in [eventIds], it is up to the caller to sort them. + * @param fromTimeline the room to forward the events from + * @param eventIds the ids of the events to forward, in the order they must be sent + * @param toRoomIds the ids of the rooms to forward the events to + * @param timeoutMs the maximum time in milliseconds to wait for an event to be sent to a room */ suspend fun forward( fromTimeline: Timeline, - eventId: EventId, + eventIds: List, toRoomIds: List, timeoutMs: Long = 5000L ) { - val messageLikeContent = (fromTimeline.getEventTimelineItemByEventId(eventId.value).content as? TimelineItemContent.MsgLike)?.content - ?: throw ForwardEventException(toRoomIds) - - val content = (messageLikeContent.kind as? MsgLikeKind.Message)?.content - ?: throw ForwardEventException(toRoomIds) - + val contents = eventIds.map { eventId -> + val timelineEvent = fromTimeline.getEventTimelineItemByEventId(eventId.value) + val content = timelineEvent.content as? TimelineItemContent.MsgLike ?: throw ForwardEventException(toRoomIds) + val message = content.content.kind as? MsgLikeKind.Message ?: throw ForwardEventException(toRoomIds) + contentWithoutRelationFromMessage(message.content) + } val targetRooms = toRoomIds.toSet().mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) } val failedForwardingTo = mutableSetOf() targetRooms.parallelMap { room -> room.use { targetRoom -> runCatchingExceptions { - // Sending a message requires a registered timeline listener - targetRoom.timeline().runWithTimelineListenerRegistered { - withTimeout(timeoutMs.milliseconds) { - targetRoom.timeline().send(contentWithoutRelationFromMessage(content)) + targetRoom.timeline().use { timeline -> + // Send the contents sequentially, so that they are received in the same order + contents.forEach { content -> + withTimeout(timeoutMs.milliseconds) { + timeline.send(content) + } } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt index c932230217..9766da8235 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt @@ -53,16 +53,3 @@ internal fun TimelineInterface.timelineDiffFlow(): Flow> = }.catch { Timber.d(it, "timelineDiffFlow() failed") }.buffer(Channel.UNLIMITED) - -internal suspend fun TimelineInterface.runWithTimelineListenerRegistered(action: suspend () -> Unit) { - val result = addListener(NoOpTimelineListener) - try { - action() - } finally { - result.cancelAndDestroy() - } -} - -private object NoOpTimelineListener : TimelineListener { - override fun onUpdate(diff: List) = Unit -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index c6a737e156..c7dd260d47 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -489,9 +489,9 @@ class RustTimeline( } } - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = withContext(dispatcher) { + override suspend fun forwardEvents(eventIds: List, roomIds: List): Result = withContext(dispatcher) { runCatchingExceptions { - roomContentForwarder.forward(fromTimeline = inner, eventId = eventId, toRoomIds = roomIds) + roomContentForwarder.forward(fromTimeline = inner, eventIds = eventIds, toRoomIds = roomIds) }.onFailure { Timber.tag(loggerTag).e(it) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index d13ac06bd3..88dcb13a8c 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -355,10 +355,10 @@ class FakeTimeline( ) } - var forwardEventLambda: (eventId: EventId, roomIds: List) -> Result = { _, _ -> lambdaError() } + var forwardEventsLambda: (eventIds: List, roomIds: List) -> Result = { _, _ -> lambdaError() } - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = simulateLongTask { - forwardEventLambda(eventId, roomIds) + override suspend fun forwardEvents(eventIds: List, roomIds: List): Result = simulateLongTask { + forwardEventsLambda(eventIds, roomIds) } var createPollLambda: (