From 54780f0f70f42bce6d65ee82740f4c450ff54ce0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Aug 2026 19:07:55 +0200 Subject: [PATCH] Add KDoc to matrix API timeline interfaces Document Timeline and all of its members, TimelineProvider, and the three lazy providers carried by EventTimelineItem. Behaviours taken from RustTimeline: collecting membershipChangeEventReceived or onSyncedEventReceived is what keeps the timeline subscribed to the SDK, paginate fails with CannotPaginate when the matching status forbids it, the initial hasMoreToLoad of each pagination status depends on the timeline mode, timelineItems also carries virtual items such as date separators, and the media send functions return a handler the caller must await. Co-Authored-By: Claude Opus 5 (1M context) --- .../libraries/matrix/api/timeline/Timeline.kt | 171 +++++++++++++++++- .../matrix/api/timeline/TimelineProvider.kt | 1 + .../timeline/item/event/EventTimelineItem.kt | 16 ++ 3 files changed, 187 insertions(+), 1 deletion(-) 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 8cb933205b..72483e68f1 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 @@ -31,6 +31,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.parcelize.Parcelize import java.io.File +/** + * A window over the events of a room, restricted by its [Mode], and the entry point for sending events to that room. + * + * A timeline owns SDK resources and must be closed once no longer needed, except the live timeline, which is owned by its room. + * Sending returns successfully as soon as the event has been handed to the room's send queue, not when the server has accepted it. + */ interface Timeline : AutoCloseable { data class PaginationStatus( val isPaginating: Boolean, @@ -54,18 +60,66 @@ interface Timeline : AutoCloseable { data class Thread(val threadRootId: ThreadId) : Mode } + /** What this timeline is a view over: the live room, a single event and its context, the pinned events, the media, or a thread. */ val mode: Mode + + /** + * Emits every time a membership change event is received in this timeline, without saying which one. + * Collecting it keeps the timeline subscribed to the SDK, so it should not be collected for longer than needed. + */ val membershipChangeEventReceived: Flow + + /** + * Emits every time an event coming from sync is received in this timeline, without saying which one. + * As with [membershipChangeEventReceived], collecting it keeps the timeline subscribed to the SDK. + */ val onSyncedEventReceived: Flow + + /** + * Sends a read receipt for a single event, which is how the unread state of the room is cleared. + * + * @param eventId the event the receipt points at. + * @param receiptType whether the receipt is public, private or only marks the fully read position. + */ suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result + + /** + * Marks the whole timeline as read by sending a receipt on its latest event. + * + * @param receiptType whether the receipt is public, private or only marks the fully read position. + */ suspend fun markAsRead(receiptType: ReceiptType): Result + + /** + * Loads one more page of events and updates the matching pagination status flow. + * Fails with a `CannotPaginate` error when the corresponding status does not allow it, for instance because a pagination is already running. + * + * @param direction whether to load older or newer events. + * @return whether the end of the timeline has been reached in that direction. + */ suspend fun paginate(direction: PaginationDirection): Result + /** Whether older events are being loaded and whether more remain; pinned event timelines start with nothing more to load. */ val backwardPaginationStatus: StateFlow + + /** Whether newer events are being loaded and whether more remain; only a timeline focused on an event starts with more to load. */ val forwardPaginationStatus: StateFlow + /** + * The items to display, in timeline order, re-emitted on every change. + * Beyond the room events this also carries the virtual items the UI needs, such as date separators, loading indicators and the room beginning. + */ val timelineItems: Flow> + /** + * Sends a text message to the room this timeline belongs to. + * + * @param body the message content, as plain text. + * @param htmlBody the message content as HTML, or `null` to send it unformatted. + * @param intentionalMentions the users and rooms the message deliberately mentions, so that they are notified. + * @param msgType the Matrix message type to use, which distinguishes a normal message from an emote or a notice. + * @param asPlainText whether to send the message without any formatting, ignoring markdown in [body]. + */ suspend fun sendMessage( body: String, htmlBody: String?, @@ -74,6 +128,14 @@ interface Timeline : AutoCloseable { asPlainText: Boolean = false, ): Result + /** + * Replaces the content of a message, whether it has already been sent or is still local in the send queue. + * + * @param eventOrTransactionId the event to edit, identified by its event id once sent or by its transaction id while still local. + * @param body the new content, as plain text. + * @param htmlBody the new content as HTML, or `null` to send it unformatted. + * @param intentionalMentions the users and rooms the new content deliberately mentions. + */ suspend fun editMessage( eventOrTransactionId: EventOrTransactionId, body: String, @@ -81,12 +143,29 @@ interface Timeline : AutoCloseable { intentionalMentions: List, ): Result + /** + * Replaces the caption of an already sent media message, leaving the media itself untouched. + * + * @param eventOrTransactionId the media event to edit, identified by its event id or by its transaction id while still local. + * @param caption the new caption, or `null` to remove it. + * @param formattedCaption the new caption as HTML, or `null` to send it unformatted. + */ suspend fun editCaption( eventOrTransactionId: EventOrTransactionId, caption: String?, formattedCaption: String?, ): Result + /** + * Sends a message as a reply to another event. + * + * @param repliedToEventId the event being replied to. + * @param body the reply content, as plain text. + * @param htmlBody the reply content as HTML, or `null` to send it unformatted. + * @param intentionalMentions the users and rooms the reply deliberately mentions. + * @param fromNotification whether the reply was written from a notification rather than from the timeline. + * @param msgType the Matrix message type to use. + */ suspend fun replyMessage( repliedToEventId: EventId, body: String, @@ -96,6 +175,16 @@ interface Timeline : AutoCloseable { msgType: MsgType = MsgType.MSG_TYPE_TEXT, ): Result + /** + * Starts sending an image. The returned [MediaUploadHandler] tracks the upload and must be awaited by the caller to know the outcome. + * + * @param file the image to upload. + * @param thumbnailFile a pre-generated thumbnail, or `null` to send none. + * @param imageInfo the dimensions, size and MIME type of the image. + * @param caption the text shown along with the image, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendImage( file: File, thumbnailFile: File?, @@ -105,6 +194,16 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending a video, with the same upload contract as [sendImage]. + * + * @param file the video to upload. + * @param thumbnailFile a pre-generated thumbnail, or `null` to send none. + * @param videoInfo the duration, dimensions, size and MIME type of the video. + * @param caption the text shown along with the video, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendVideo( file: File, thumbnailFile: File?, @@ -114,6 +213,15 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending an audio file, with the same upload contract as [sendImage]; use [sendVoiceMessage] for a recorded voice message. + * + * @param file the audio file to upload. + * @param audioInfo the duration, size and MIME type of the audio. + * @param caption the text shown along with the audio, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendAudio( file: File, audioInfo: AudioInfo, @@ -122,6 +230,15 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending an arbitrary file, with the same upload contract as [sendImage]. + * + * @param file the file to upload. + * @param fileInfo the size and MIME type of the file. + * @param caption the text shown along with the file, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendFile( file: File, fileInfo: FileInfo, @@ -151,6 +268,14 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending a recorded voice message, with the same upload contract as [sendImage]. + * + * @param file the recorded audio to upload. + * @param audioInfo the duration, size and MIME type of the recording. + * @param waveform the amplitudes used to draw the waveform, normalised between 0 and 1. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendVoiceMessage( file: File, audioInfo: AudioInfo, @@ -158,6 +283,14 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending several media items as a single gallery message, with the same upload contract as [sendImage]. + * + * @param items the media making up the gallery, in the order they should appear. + * @param caption the text shown along with the gallery, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendGallery( items: List, caption: String?, @@ -165,12 +298,36 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Redacts an event, i.e. asks the server to remove its content; it also removes a local echo that has not been sent yet. + * + * @param eventOrTransactionId the event to redact, identified by its event id once sent or by its transaction id while still local. + * @param reason the explanation recorded in the redaction event, or `null` for none. + */ suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result + /** + * Adds the given reaction to an event, or removes it if the current user had already reacted with it. + * + * @param emoji the reaction key, usually an emoji. + * @param eventOrTransactionId the event to react to, identified by its event id or by its transaction id while still local. + * @return whether the reaction is now present. + */ 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. + * + * @param eventId the event whose content is forwarded. + * @param roomIds the rooms to forward it to. + */ suspend fun forwardEvent(eventId: EventId, roomIds: List): Result + /** + * Cancels an event that is still waiting in the send queue, by redacting its local echo. + * + * @param transactionId the transaction id of the local echo to drop. + */ suspend fun cancelSend(transactionId: TransactionId): Result = redactEvent(transactionId.toEventOrTransactionId(), reason = null) @@ -222,12 +379,20 @@ interface Timeline : AutoCloseable { */ suspend fun endPoll(pollStartId: EventId, text: String): Result + /** + * Resolves the content of a replied-to event so it can be rendered above the reply. + * Served from the loaded timeline items when possible, otherwise fetched through the SDK. + * + * @param eventId the event being replied to. + */ suspend fun loadReplyDetails(eventId: EventId): InReplyTo /** * Returns true if [eventId] is currently loaded in this timeline's window, even if the display * layer does not render it (e.g. filtered or state events). This distinguishes "in the window * but not displayed" from "genuinely outside the loaded window". + * + * @param eventId the event to look for. */ suspend fun isEventLoaded(eventId: EventId): Boolean @@ -237,6 +402,8 @@ interface Timeline : AutoCloseable { * * Returns `true` if we sent the request, `false` if the event was already * pinned. + * + * @param eventId the event to pin. */ suspend fun pinEvent(eventId: EventId): Result @@ -246,11 +413,13 @@ interface Timeline : AutoCloseable { * * Returns `true` if we sent the request, `false` if the event wasn't * pinned + * + * @param eventId the event to unpin. */ suspend fun unpinEvent(eventId: EventId): Result /** - * Get the latest event id of the timeline. + * Get the latest event id of the timeline, or `null` when it holds no event yet. */ suspend fun getLatestEventId(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt index 28e487f527..ef9d5d794e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.first * By default, the active timeline is the live timeline. */ fun interface TimelineProvider { + /** The timeline currently in use, or `null` while none is available yet; see [getActiveTimeline] to await a non-null value. */ fun activeTimelineFlow(): StateFlow } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index f3982e83d2..91703f7fa4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -60,14 +60,30 @@ data class EventTimelineItem( } } +/** + * Provides the debug information of a timeline item on demand, so the original event JSON is only fetched from the SDK when actually displayed. + */ fun interface TimelineItemDebugInfoProvider { + /** Returns the debug information of the item, including its original and latest edit JSON. */ operator fun invoke(): TimelineItemDebugInfo } +/** + * Provides the encryption warning to show next to a timeline item on demand, so the check is only run for items that are displayed. + */ fun interface MessageShieldProvider { + /** + * Returns the warning to display for this event, or `null` when there is nothing to warn about. + * + * @param strict whether to also report the weaker problems that are hidden by default. + */ operator fun invoke(strict: Boolean): MessageShield? } +/** + * Provides the send queue handle of a timeline item on demand, which is needed to retry or cancel a failed send. + */ fun interface SendHandleProvider { + /** Returns the send handle of the item, or `null` when it is not a local echo waiting in the send queue. */ operator fun invoke(): SendHandle? }