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

@ -20,3 +20,5 @@ value class EventId(val value: String) : Serializable {
override fun toString(): String = value
}
fun EventId.toThreadId(): ThreadId = ThreadId(value)

View file

@ -8,10 +8,12 @@
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ThreadId
sealed interface CreateTimelineParams {
data class Focused(val focusedEventId: EventId) : CreateTimelineParams
data object MediaOnly : CreateTimelineParams
data class MediaOnlyFocused(val focusedEventId: EventId) : CreateTimelineParams
data object PinnedOnly : CreateTimelineParams
data class Threaded(val threadRootEventId: ThreadId) : CreateTimelineParams
}

View file

@ -7,8 +7,10 @@
package io.element.android.libraries.matrix.api.timeline
import android.os.Parcelable
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.ThreadId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
@ -23,6 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.parcelize.Parcelize
import java.io.File
interface Timeline : AutoCloseable {
@ -38,13 +41,16 @@ interface Timeline : AutoCloseable {
FORWARDS
}
enum class Mode {
LIVE,
FOCUSED_ON_EVENT,
PINNED_EVENTS,
MEDIA,
@Parcelize
sealed interface Mode : Parcelable {
data object Live : Mode
data class FocusedOnEvent(val eventId: EventId) : Mode
data object PinnedEvents : Mode
data object Media : Mode
data class Thread(val threadRootId: ThreadId) : Mode
}
val mode: Mode
val membershipChangeEventReceived: Flow<Unit>
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
suspend fun paginate(direction: PaginationDirection): Result<Boolean>

View file

@ -0,0 +1,33 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.timeline.item
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
data class EventThreadInfo(
val threadRootId: ThreadId?,
val threadSummary: ThreadSummary?,
)
data class ThreadSummary(
val latestEvent: AsyncData<EmbeddedEventInfo>,
val numberOfReplies: Long,
)
data class EmbeddedEventInfo(
val eventOrTransactionId: EventOrTransactionId,
val content: EventContent,
val senderId: UserId,
val senderProfile: ProfileTimelineDetails,
val timestamp: Long,
)

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@ -23,7 +24,7 @@ data class MessageContent(
val body: String,
val inReplyTo: InReplyTo?,
val isEdited: Boolean,
val isThreaded: Boolean,
val threadInfo: EventThreadInfo,
val type: MessageType
) : EventContent

View file

@ -11,6 +11,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SendHandle
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import kotlinx.collections.immutable.ImmutableList
@ -37,9 +38,7 @@ data class EventTimelineItem(
return (content as? MessageContent)?.inReplyTo
}
fun isThreaded(): Boolean {
return (content as? MessageContent)?.isThreaded ?: false
}
fun threadInfo(): EventThreadInfo? = (content as? MessageContent)?.threadInfo
fun hasNotLoadedInReplyTo(): Boolean {
val details = inReplyTo()