Threads - first iteration (#5165)

* Initial threads support: parse `ThreadSummary`.

Replace several `isThreaded` values with `EventThreadInfo`, which contains the info about the event either being the root of a thread or part of it.

* Add `Threaded` timeline mode

* Add a `liveTimeline` parameter to `TimelineController`'s  constructor. This way we can customise which timeline will be used as the 'live' one. Also add `@LiveTimeline` DI qualifier for the actual live timeline of the room.

* Create `ThreadedMessagesNode`. Allow opening a thread in a separate screen.

* Add the callbacks for the list menu actions - even if they're the wrong ones and will send the data to the room instead

* Send attachments and location in threads

* Fix polls in threads, add support for sending voice messages in threads

* Display thread summaries only when the feature flag is enabled

* Use 'Reply' instead of 'Reply in thread' when in threaded timeline mode

* Remove incorrect usage of `Timeline` in `MessageComposerPresenter`. This led to replies to threaded events not appearing as actual replies.

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2025-08-19 15:35:48 +02:00 committed by GitHub
parent cc10ba41fd
commit 35928e3630
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
119 changed files with 1520 additions and 339 deletions

View file

@ -8,21 +8,33 @@
package io.element.android.libraries.mediaupload.api
import android.net.Uri
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.extensions.flatMapCatching
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
class MediaSender @Inject constructor(
class MediaSender @AssistedInject constructor(
private val preProcessor: MediaPreProcessor,
private val room: JoinedRoom,
@Assisted private val timelineMode: Timeline.Mode,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
) {
@AssistedFactory
interface Factory {
fun create(
timelineMode: Timeline.Mode,
): MediaSender
}
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
@ -46,12 +58,14 @@ class MediaSender @Inject constructor(
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<Unit> {
return room.liveTimeline.sendMedia(
uploadInfo = mediaUploadInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
return getTimeline().flatMap {
it.sendMedia(
uploadInfo = mediaUploadInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
.handleSendResult()
}
@ -71,7 +85,7 @@ class MediaSender @Inject constructor(
mediaOptimizationConfig = mediaOptimizationConfig,
)
.flatMapCatching { info ->
room.liveTimeline.sendMedia(
getTimeline().getOrThrow().sendMedia(
uploadInfo = info,
caption = caption,
formattedCaption = formattedCaption,
@ -101,7 +115,7 @@ class MediaSender @Inject constructor(
audioInfo = audioInfo,
waveform = waveForm,
)
room.liveTimeline.sendMedia(
getTimeline().getOrThrow().sendMedia(
uploadInfo = newInfo,
caption = null,
formattedCaption = null,
@ -186,6 +200,15 @@ class MediaSender @Inject constructor(
}
}
private suspend fun getTimeline(): Result<Timeline> {
return when (timelineMode) {
is Timeline.Mode.Thread -> {
room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = timelineMode.threadRootId))
}
else -> Result.success(room.liveTimeline)
}
}
/**
* Clean up any temporary files or resources used during the media processing.
*/

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
@ -160,6 +161,7 @@ class MediaSenderTest {
) = MediaSender(
preProcessor = preProcessor,
room = room,
timelineMode = Timeline.Mode.Live,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
)
}