matrix_sdk_ui/timeline/controller/
metadata.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    collections::{BTreeSet, HashMap},
17    num::NonZeroUsize,
18    sync::Arc,
19};
20
21use matrix_sdk::ring_buffer::RingBuffer;
22use ruma::{EventId, OwnedEventId, OwnedUserId, RoomVersionId};
23use tracing::trace;
24
25use super::{
26    super::{
27        rfind_event_by_id, subscriber::skip::SkipCount, TimelineItem, TimelineItemKind,
28        TimelineUniqueId,
29    },
30    read_receipts::ReadReceipts,
31    Aggregations, AllRemoteEvents, ObservableItemsTransaction, PendingEdit,
32};
33use crate::unable_to_decrypt_hook::UtdHookManager;
34
35#[derive(Clone, Debug)]
36pub(in crate::timeline) struct TimelineMetadata {
37    // **** CONSTANT FIELDS ****
38    /// An optional prefix for internal IDs, defined during construction of the
39    /// timeline.
40    ///
41    /// This value is constant over the lifetime of the metadata.
42    internal_id_prefix: Option<String>,
43
44    /// The `count` value for the `Skip higher-order stream used by the
45    /// `TimelineSubscriber`. See its documentation to learn more.
46    pub(super) subscriber_skip_count: SkipCount,
47
48    /// The hook to call whenever we run into a unable-to-decrypt event.
49    ///
50    /// This value is constant over the lifetime of the metadata.
51    pub unable_to_decrypt_hook: Option<Arc<UtdHookManager>>,
52
53    /// A boolean indicating whether the room the timeline is attached to is
54    /// actually encrypted or not.
55    ///
56    /// May be false until we fetch the actual room encryption state.
57    pub is_room_encrypted: bool,
58
59    /// Matrix room version of the timeline's room, or a sensible default.
60    ///
61    /// This value is constant over the lifetime of the metadata.
62    pub room_version: RoomVersionId,
63
64    /// The own [`OwnedUserId`] of the client who opened the timeline.
65    own_user_id: OwnedUserId,
66
67    // **** DYNAMIC FIELDS ****
68    /// The next internal identifier for timeline items, used for both local and
69    /// remote echoes.
70    ///
71    /// This is never cleared, but always incremented, to avoid issues with
72    /// reusing a stale internal id across timeline clears. We don't expect
73    /// we can hit `u64::max_value()` realistically, but if this would
74    /// happen, we do a wrapping addition when incrementing this
75    /// id; the previous 0 value would have disappeared a long time ago, unless
76    /// the device has terabytes of RAM.
77    next_internal_id: u64,
78
79    /// Aggregation metadata and pending aggregations.
80    pub aggregations: Aggregations,
81
82    /// Given an event, what are all the events that are replies to it?
83    pub replies: HashMap<OwnedEventId, BTreeSet<OwnedEventId>>,
84
85    /// Edit events received before the related event they're editing.
86    pub pending_edits: RingBuffer<PendingEdit>,
87
88    /// Identifier of the fully-read event, helping knowing where to introduce
89    /// the read marker.
90    pub fully_read_event: Option<OwnedEventId>,
91
92    /// Whether we have a fully read-marker item in the timeline, that's up to
93    /// date with the room's read marker.
94    ///
95    /// This is false when:
96    /// - The fully-read marker points to an event that is not in the timeline,
97    /// - The fully-read marker item would be the last item in the timeline.
98    pub has_up_to_date_read_marker_item: bool,
99
100    /// Read receipts related state.
101    ///
102    /// TODO: move this over to the event cache (see also #3058).
103    pub(super) read_receipts: ReadReceipts,
104}
105
106/// Maximum number of stash pending edits.
107/// SAFETY: 32 is not 0.
108const MAX_NUM_STASHED_PENDING_EDITS: NonZeroUsize = NonZeroUsize::new(32).unwrap();
109
110impl TimelineMetadata {
111    pub(in crate::timeline) fn new(
112        own_user_id: OwnedUserId,
113        room_version: RoomVersionId,
114        internal_id_prefix: Option<String>,
115        unable_to_decrypt_hook: Option<Arc<UtdHookManager>>,
116        is_room_encrypted: bool,
117    ) -> Self {
118        Self {
119            subscriber_skip_count: SkipCount::new(),
120            own_user_id,
121            next_internal_id: Default::default(),
122            aggregations: Default::default(),
123            pending_edits: RingBuffer::new(MAX_NUM_STASHED_PENDING_EDITS),
124            replies: Default::default(),
125            fully_read_event: Default::default(),
126            // It doesn't make sense to set this to false until we fill the `fully_read_event`
127            // field, otherwise we'll keep on exiting early in `Self::update_read_marker`.
128            has_up_to_date_read_marker_item: true,
129            read_receipts: Default::default(),
130            room_version,
131            unable_to_decrypt_hook,
132            internal_id_prefix,
133            is_room_encrypted,
134        }
135    }
136
137    pub(super) fn clear(&mut self) {
138        // Note: we don't clear the next internal id to avoid bad cases of stale unique
139        // ids across timeline clears.
140        self.aggregations.clear();
141        self.replies.clear();
142        self.pending_edits.clear();
143        self.fully_read_event = None;
144        // We forgot about the fully read marker right above, so wait for a new one
145        // before attempting to update it for each new timeline item.
146        self.has_up_to_date_read_marker_item = true;
147        self.read_receipts.clear();
148    }
149
150    /// Get the relative positions of two events in the timeline.
151    ///
152    /// This method assumes that all events since the end of the timeline are
153    /// known.
154    ///
155    /// Returns `None` if none of the two events could be found in the timeline.
156    pub(in crate::timeline) fn compare_events_positions(
157        &self,
158        event_a: &EventId,
159        event_b: &EventId,
160        all_remote_events: &AllRemoteEvents,
161    ) -> Option<RelativePosition> {
162        if event_a == event_b {
163            return Some(RelativePosition::Same);
164        }
165
166        // We can make early returns here because we know all events since the end of
167        // the timeline, so the first event encountered is the oldest one.
168        for event_meta in all_remote_events.iter().rev() {
169            if event_meta.event_id == event_a {
170                return Some(RelativePosition::Before);
171            }
172            if event_meta.event_id == event_b {
173                return Some(RelativePosition::After);
174            }
175        }
176
177        None
178    }
179
180    /// Returns the next internal id for a timeline item (and increment our
181    /// internal counter).
182    fn next_internal_id(&mut self) -> TimelineUniqueId {
183        let val = self.next_internal_id;
184        self.next_internal_id = self.next_internal_id.wrapping_add(1);
185        let prefix = self.internal_id_prefix.as_deref().unwrap_or("");
186        TimelineUniqueId(format!("{prefix}{val}"))
187    }
188
189    /// Returns a new timeline item with a fresh internal id.
190    pub fn new_timeline_item(&mut self, kind: impl Into<TimelineItemKind>) -> Arc<TimelineItem> {
191        TimelineItem::new(kind, self.next_internal_id())
192    }
193
194    /// Try to update the read marker item in the timeline.
195    pub(crate) fn update_read_marker(&mut self, items: &mut ObservableItemsTransaction<'_>) {
196        let Some(fully_read_event) = &self.fully_read_event else { return };
197        trace!(?fully_read_event, "Updating read marker");
198
199        let read_marker_idx = items.iter().rposition(|item| item.is_read_marker());
200
201        let mut fully_read_event_idx =
202            rfind_event_by_id(items, fully_read_event).map(|(idx, _)| idx);
203
204        if let Some(i) = &mut fully_read_event_idx {
205            // The item at position `i` is the first item that's fully read, we're about to
206            // insert a read marker just after it.
207            //
208            // Do another forward pass to skip all the events we've sent too.
209
210            // Find the position of the first element…
211            let next = items
212                .iter()
213                .enumerate()
214                // …strictly *after* the fully read event…
215                .skip(*i + 1)
216                // …that's not virtual and not sent by us…
217                .find(|(_, item)| {
218                    item.as_event().is_some_and(|event| event.sender() != self.own_user_id)
219                })
220                .map(|(i, _)| i);
221
222            if let Some(next) = next {
223                // `next` point to the first item that's not sent by us, so the *previous* of
224                // next is the right place where to insert the fully read marker.
225                *i = next.wrapping_sub(1);
226            } else {
227                // There's no event after the read marker that's not sent by us, i.e. the full
228                // timeline has been read: the fully read marker goes to the end.
229                *i = items.len().wrapping_sub(1);
230            }
231        }
232
233        match (read_marker_idx, fully_read_event_idx) {
234            (None, None) => {
235                // We didn't have a previous read marker, and we didn't find the fully-read
236                // event in the timeline items. Don't do anything, and retry on
237                // the next event we add.
238                self.has_up_to_date_read_marker_item = false;
239            }
240
241            (None, Some(idx)) => {
242                // Only insert the read marker if it is not at the end of the timeline.
243                if idx + 1 < items.len() {
244                    let idx = idx + 1;
245                    items.insert(idx, TimelineItem::read_marker(), None);
246                    self.has_up_to_date_read_marker_item = true;
247                } else {
248                    // The next event might require a read marker to be inserted at the current
249                    // end.
250                    self.has_up_to_date_read_marker_item = false;
251                }
252            }
253
254            (Some(_), None) => {
255                // We didn't find the timeline item containing the event referred to by the read
256                // marker. Retry next time we get a new event.
257                self.has_up_to_date_read_marker_item = false;
258            }
259
260            (Some(from), Some(to)) => {
261                if from >= to {
262                    // The read marker can't move backwards.
263                    if from + 1 == items.len() {
264                        // The read marker has nothing after it. An item disappeared; remove it.
265                        items.remove(from);
266                    }
267                    self.has_up_to_date_read_marker_item = true;
268                    return;
269                }
270
271                let prev_len = items.len();
272                let read_marker = items.remove(from);
273
274                // Only insert the read marker if it is not at the end of the timeline.
275                if to + 1 < prev_len {
276                    // Since the fully-read event's index was shifted to the left
277                    // by one position by the remove call above, insert the fully-
278                    // read marker at its previous position, rather than that + 1
279                    items.insert(to, read_marker, None);
280                    self.has_up_to_date_read_marker_item = true;
281                } else {
282                    self.has_up_to_date_read_marker_item = false;
283                }
284            }
285        }
286    }
287}
288
289/// Result of comparing events position in the timeline.
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub(in crate::timeline) enum RelativePosition {
292    /// Event B is after (more recent than) event A.
293    After,
294    /// They are the same event.
295    Same,
296    /// Event B is before (older than) event A.
297    Before,
298}
299
300/// Metadata about an event that needs to be kept in memory.
301#[derive(Debug, Clone)]
302pub(in crate::timeline) struct EventMeta {
303    /// The ID of the event.
304    pub event_id: OwnedEventId,
305
306    /// Whether the event is among the timeline items.
307    pub visible: bool,
308
309    /// Foundation for the mapping between remote events to timeline items.
310    ///
311    /// Let's explain it. The events represent the first set and are stored in
312    /// [`ObservableItems::all_remote_events`], and the timeline
313    /// items represent the second set and are stored in
314    /// [`ObservableItems::items`].
315    ///
316    /// Each event is mapped to at most one timeline item:
317    ///
318    /// - `None` if the event isn't rendered in the timeline (e.g. some state
319    ///   events, or malformed events) or is rendered as a timeline item that
320    ///   attaches to or groups with another item, like reactions,
321    /// - `Some(_)` if the event is rendered in the timeline.
322    ///
323    /// This is neither a surjection nor an injection. Every timeline item may
324    /// not be attached to an event, for example with a virtual timeline item.
325    /// We can formulate other rules:
326    ///
327    /// - a timeline item that doesn't _move_ and that is represented by an
328    ///   event has a mapping to an event,
329    /// - a virtual timeline item has no mapping to an event.
330    ///
331    /// Imagine the following remote events:
332    ///
333    /// | index | remote events |
334    /// +-------+---------------+
335    /// | 0     | `$ev0`        |
336    /// | 1     | `$ev1`        |
337    /// | 2     | `$ev2`        |
338    /// | 3     | `$ev3`        |
339    /// | 4     | `$ev4`        |
340    /// | 5     | `$ev5`        |
341    ///
342    /// Once rendered in a timeline, it for example produces:
343    ///
344    /// | index | item              | related items        |
345    /// +-------+-------------------+----------------------+
346    /// | 0     | content of `$ev0` |                      |
347    /// | 1     | content of `$ev2` | reaction with `$ev4` |
348    /// | 2     | date divider      |                      |
349    /// | 3     | content of `$ev3` |                      |
350    /// | 4     | content of `$ev5` |                      |
351    ///
352    /// Note the date divider that is a virtual item. Also note `$ev4` which is
353    /// a reaction to `$ev2`. Finally note that `$ev1` is not rendered in
354    /// the timeline.
355    ///
356    /// The mapping between remote event index to timeline item index will look
357    /// like this:
358    ///
359    /// | remote event index | timeline item index | comment                                    |
360    /// +--------------------+---------------------+--------------------------------------------+
361    /// | 0                  | `Some(0)`           | `$ev0` is rendered as the #0 timeline item |
362    /// | 1                  | `None`              | `$ev1` isn't rendered in the timeline      |
363    /// | 2                  | `Some(1)`           | `$ev2` is rendered as the #1 timeline item |
364    /// | 3                  | `Some(3)`           | `$ev3` is rendered as the #3 timeline item |
365    /// | 4                  | `None`              | `$ev4` is a reaction to item #1            |
366    /// | 5                  | `Some(4)`           | `$ev5` is rendered as the #4 timeline item |
367    ///
368    /// Note that the #2 timeline item (the day divider) doesn't map to any
369    /// remote event, but if it moves, it has an impact on this mapping.
370    pub timeline_item_index: Option<usize>,
371}