Forward several events at once

This commit is contained in:
ganfra
2026-09-25 18:47:17 +02:00
parent 51c06b4bec
commit 93831ffcbc
11 changed files with 50 additions and 62 deletions
@@ -23,7 +23,7 @@ interface ForwardEntryPoint : FeatureEntryPoint {
} }
data class Params( data class Params(
val eventId: EventId, val eventIds: List<EventId>,
val timelineProvider: TimelineProvider, val timelineProvider: TimelineProvider,
) : NodeInputs ) : NodeInputs
@@ -27,7 +27,7 @@ class DefaultForwardEntryPoint : ForwardEntryPoint {
buildContext = buildContext, buildContext = buildContext,
plugins = listOf( plugins = listOf(
ForwardMessagesNode.Inputs( ForwardMessagesNode.Inputs(
eventId = params.eventId, eventIds = params.eventIds,
timelineProvider = params.timelineProvider, timelineProvider = params.timelineProvider,
), ),
callback, callback,
@@ -52,13 +52,13 @@ class ForwardMessagesNode(
object NavTarget : Parcelable object NavTarget : Parcelable
data class Inputs( data class Inputs(
val eventId: EventId, val eventIds: List<EventId>,
val timelineProvider: TimelineProvider, val timelineProvider: TimelineProvider,
) : NodeInputs ) : NodeInputs
private val inputs = inputs<Inputs>() private val inputs = inputs<Inputs>()
private val callback: ForwardEntryPoint.Callback = callback() 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 { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
val callback = object : RoomSelectEntryPoint.Callback { val callback = object : RoomSelectEntryPoint.Callback {
@@ -28,22 +28,20 @@ import timber.log.Timber
@AssistedInject @AssistedInject
class ForwardMessagesPresenter( class ForwardMessagesPresenter(
@Assisted eventId: String, @Assisted private val eventIds: List<EventId>,
@Assisted private val timelineProvider: TimelineProvider, @Assisted private val timelineProvider: TimelineProvider,
@SessionCoroutineScope @SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
) : Presenter<ForwardMessagesState> { ) : Presenter<ForwardMessagesState> {
private val eventId: EventId = EventId(eventId)
@AssistedFactory @AssistedFactory
fun interface Factory { fun interface Factory {
fun create(eventId: String, timelineProvider: TimelineProvider): ForwardMessagesPresenter fun create(eventIds: List<EventId>, timelineProvider: TimelineProvider): ForwardMessagesPresenter
} }
private val forwardingActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized) private val forwardingActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized)
fun onRoomSelected(roomIds: List<RoomId>) { fun onRoomSelected(roomIds: List<RoomId>) {
sessionCoroutineScope.forwardEvent(eventId, roomIds) sessionCoroutineScope.forwardEvents(eventIds, roomIds)
} }
@Composable @Composable
@@ -60,14 +58,14 @@ class ForwardMessagesPresenter(
) )
} }
private fun CoroutineScope.forwardEvent( private fun CoroutineScope.forwardEvents(
eventId: EventId, eventIds: List<EventId>,
roomIds: List<RoomId>, roomIds: List<RoomId>,
) = launch { ) = launch {
suspend { suspend {
timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds) timelineProvider.getActiveTimeline().forwardEvents(eventIds, roomIds)
.onFailure { .onFailure {
Timber.e(it, "Error while forwarding event") Timber.e(it, "Error while forwarding events")
} }
.getOrThrow() .getOrThrow()
roomIds roomIds
@@ -45,7 +45,7 @@ class DefaultForwardEntryPointTest {
override fun onDone(roomIds: List<RoomId>) = lambdaError() override fun onDone(roomIds: List<RoomId>) = lambdaError()
} }
val params = ForwardEntryPoint.Params( val params = ForwardEntryPoint.Params(
eventId = AN_EVENT_ID, eventIds = listOf(AN_EVENT_ID),
timelineProvider = FakeTimelineProvider(), timelineProvider = FakeTimelineProvider(),
) )
val result = entryPoint.createNode( val result = entryPoint.createNode(
@@ -57,7 +57,7 @@ class DefaultForwardEntryPointTest {
assertThat(result).isInstanceOf(ForwardMessagesNode::class.java) assertThat(result).isInstanceOf(ForwardMessagesNode::class.java)
assertThat(result.plugins).contains( assertThat(result.plugins).contains(
ForwardMessagesNode.Inputs( ForwardMessagesNode.Inputs(
eventId = params.eventId, eventIds = params.eventIds,
timelineProvider = params.timelineProvider, timelineProvider = params.timelineProvider,
) )
) )
@@ -44,11 +44,11 @@ class ForwardMessagesPresenterTest {
@Test @Test
fun `present - forward successful`() = runTest { fun `present - forward successful`() = runTest {
val forwardEventLambda = lambdaRecorder { _: EventId, _: List<RoomId> -> val forwardEventsLambda = lambdaRecorder { _: List<EventId>, _: List<RoomId> ->
Result.success(Unit) Result.success(Unit)
} }
val timeline = FakeTimeline().apply { val timeline = FakeTimeline().apply {
this.forwardEventLambda = forwardEventLambda this.forwardEventsLambda = forwardEventsLambda
} }
val room = FakeJoinedRoom(liveTimeline = timeline) val room = FakeJoinedRoom(liveTimeline = timeline)
val presenter = createForwardMessagesPresenter(fakeRoom = room) val presenter = createForwardMessagesPresenter(fakeRoom = room)
@@ -62,17 +62,17 @@ class ForwardMessagesPresenterTest {
assertThat(forwardingState.forwardAction.isLoading()).isTrue() assertThat(forwardingState.forwardAction.isLoading()).isTrue()
val successfulForwardState = awaitItem() val successfulForwardState = awaitItem()
assertThat(successfulForwardState.forwardAction).isEqualTo(AsyncAction.Success(listOf(summary.roomId))) assertThat(successfulForwardState.forwardAction).isEqualTo(AsyncAction.Success(listOf(summary.roomId)))
forwardEventLambda.assertions().isCalledOnce() forwardEventsLambda.assertions().isCalledOnce()
} }
} }
@Test @Test
fun `present - select a room and forward failed, then clear`() = runTest { fun `present - select a room and forward failed, then clear`() = runTest {
val forwardEventLambda = lambdaRecorder { _: EventId, _: List<RoomId> -> val forwardEventsLambda = lambdaRecorder { _: List<EventId>, _: List<RoomId> ->
Result.failure<Unit>(IllegalStateException("error")) Result.failure<Unit>(IllegalStateException("error"))
} }
val timeline = FakeTimeline().apply { val timeline = FakeTimeline().apply {
this.forwardEventLambda = forwardEventLambda this.forwardEventsLambda = forwardEventsLambda
} }
val room = FakeJoinedRoom(liveTimeline = timeline) val room = FakeJoinedRoom(liveTimeline = timeline)
val presenter = createForwardMessagesPresenter(fakeRoom = room) val presenter = createForwardMessagesPresenter(fakeRoom = room)
@@ -88,16 +88,16 @@ class ForwardMessagesPresenterTest {
// Then clear error // Then clear error
failedForwardState.eventSink(ForwardMessagesEvent.ClearError) failedForwardState.eventSink(ForwardMessagesEvent.ClearError)
assertThat(awaitItem().forwardAction.isUninitialized()).isTrue() assertThat(awaitItem().forwardAction.isUninitialized()).isTrue()
forwardEventLambda.assertions().isCalledOnce() forwardEventsLambda.assertions().isCalledOnce()
} }
} }
} }
fun TestScope.createForwardMessagesPresenter( fun TestScope.createForwardMessagesPresenter(
eventId: EventId = AN_EVENT_ID, eventIds: List<EventId> = listOf(AN_EVENT_ID),
fakeRoom: FakeJoinedRoom = FakeJoinedRoom(), fakeRoom: FakeJoinedRoom = FakeJoinedRoom(),
) = ForwardMessagesPresenter( ) = ForwardMessagesPresenter(
eventId = eventId.value, eventIds = eventIds,
timelineProvider = LiveTimelineProvider(fakeRoom), timelineProvider = LiveTimelineProvider(fakeRoom),
sessionCoroutineScope = this, sessionCoroutineScope = this,
) )
@@ -316,12 +316,13 @@ interface Timeline : AutoCloseable {
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Boolean> suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Boolean>
/** /**
* 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 eventIds the events whose content is forwarded.
* @param roomIds the rooms to forward it to. * @param roomIds the rooms to forward them to.
*/ */
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> suspend fun forwardEvents(eventIds: List<EventId>, roomIds: List<RoomId>): Result<Unit>
/** /**
* Cancels an event that is still waiting in the send queue, by redacting its local echo. * Cancels an event that is still waiting in the send queue, by redacting its local echo.
@@ -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.core.RoomId
import io.element.android.libraries.matrix.api.room.ForwardEventException 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.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.MsgLikeKind import org.matrix.rustcomponents.sdk.MsgLikeKind
import org.matrix.rustcomponents.sdk.RoomListService import org.matrix.rustcomponents.sdk.RoomListService
@@ -31,33 +30,36 @@ class RoomContentForwarder(
private val roomListService: RoomListService, private val roomListService: RoomListService,
) { ) {
/** /**
* Forwards the event with the given [eventId] from the [fromTimeline] to the given [toRoomIds]. * Forwards the events with the given [eventIds] from the [fromTimeline] to the given [toRoomIds].
* @param fromTimeline the room to forward the event from * 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 eventId the id of the event to forward * @param fromTimeline the room to forward the events from
* @param toRoomIds the ids of the rooms to forward the event to * @param eventIds the ids of the events to forward, in the order they must be sent
* @param timeoutMs the maximum time in milliseconds to wait for the event to be sent to a room * @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( suspend fun forward(
fromTimeline: Timeline, fromTimeline: Timeline,
eventId: EventId, eventIds: List<EventId>,
toRoomIds: List<RoomId>, toRoomIds: List<RoomId>,
timeoutMs: Long = 5000L timeoutMs: Long = 5000L
) { ) {
val messageLikeContent = (fromTimeline.getEventTimelineItemByEventId(eventId.value).content as? TimelineItemContent.MsgLike)?.content val contents = eventIds.map { eventId ->
?: throw ForwardEventException(toRoomIds) val timelineEvent = fromTimeline.getEventTimelineItemByEventId(eventId.value)
val content = timelineEvent.content as? TimelineItemContent.MsgLike ?: throw ForwardEventException(toRoomIds)
val content = (messageLikeContent.kind as? MsgLikeKind.Message)?.content val message = content.content.kind as? MsgLikeKind.Message ?: throw ForwardEventException(toRoomIds)
?: throw ForwardEventException(toRoomIds) contentWithoutRelationFromMessage(message.content)
}
val targetRooms = toRoomIds.toSet().mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) } val targetRooms = toRoomIds.toSet().mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
val failedForwardingTo = mutableSetOf<RoomId>() val failedForwardingTo = mutableSetOf<RoomId>()
targetRooms.parallelMap { room -> targetRooms.parallelMap { room ->
room.use { targetRoom -> room.use { targetRoom ->
runCatchingExceptions { runCatchingExceptions {
// Sending a message requires a registered timeline listener targetRoom.timeline().use { timeline ->
targetRoom.timeline().runWithTimelineListenerRegistered { // Send the contents sequentially, so that they are received in the same order
withTimeout(timeoutMs.milliseconds) { contents.forEach { content ->
targetRoom.timeline().send(contentWithoutRelationFromMessage(content)) withTimeout(timeoutMs.milliseconds) {
timeline.send(content)
}
} }
} }
} }
@@ -53,16 +53,3 @@ internal fun TimelineInterface.timelineDiffFlow(): Flow<List<TimelineDiff>> =
}.catch { }.catch {
Timber.d(it, "timelineDiffFlow() failed") Timber.d(it, "timelineDiffFlow() failed")
}.buffer(Channel.UNLIMITED) }.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<TimelineDiff>) = Unit
}
@@ -489,9 +489,9 @@ class RustTimeline(
} }
} }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = withContext(dispatcher) { override suspend fun forwardEvents(eventIds: List<EventId>, roomIds: List<RoomId>): Result<Unit> = withContext(dispatcher) {
runCatchingExceptions { runCatchingExceptions {
roomContentForwarder.forward(fromTimeline = inner, eventId = eventId, toRoomIds = roomIds) roomContentForwarder.forward(fromTimeline = inner, eventIds = eventIds, toRoomIds = roomIds)
}.onFailure { }.onFailure {
Timber.tag(loggerTag).e(it) Timber.tag(loggerTag).e(it)
} }
@@ -355,10 +355,10 @@ class FakeTimeline(
) )
} }
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> lambdaError() } var forwardEventsLambda: (eventIds: List<EventId>, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> lambdaError() }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = simulateLongTask { override suspend fun forwardEvents(eventIds: List<EventId>, roomIds: List<RoomId>): Result<Unit> = simulateLongTask {
forwardEventLambda(eventId, roomIds) forwardEventsLambda(eventIds, roomIds)
} }
var createPollLambda: ( var createPollLambda: (