Skip to main content

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    sync::Arc,
18};
19
20use imbl::Vector;
21use matrix_sdk::deserialized_responses::EncryptionInfo;
22use ruma::{
23    EventId, OwnedEventId, OwnedUserId, UserId,
24    events::{
25        AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
26        BundledMessageLikeRelations,
27        poll::unstable_start::UnstablePollStartEventContent,
28        relation::Replacement,
29        room::{encrypted::Relation, message::RelationWithoutReplacement},
30    },
31    room_version_rules::RoomVersionRules,
32    serde::Raw,
33};
34use tracing::trace;
35
36use super::{
37    super::{TimelineItem, TimelineItemKind, TimelineUniqueId, subscriber::skip::SkipCount},
38    ActiveCallInfo, Aggregation, AggregationKind, Aggregations, AllRemoteEvents,
39    ObservableItemsTransaction, PendingEdit, PendingEditKind,
40    read_receipts::ReadReceiptsState,
41};
42use crate::{
43    timeline::{
44        InReplyToDetails, TimelineEventItemId,
45        event_item::{
46            extract_bundled_edit_event_json, extract_poll_edit_content,
47            extract_room_msg_edit_content,
48        },
49    },
50    unable_to_decrypt_hook::UtdHookManager,
51};
52
53/// All parameters to [`TimelineAction::from_content`] that only apply if an
54/// event is a remote echo.
55pub(crate) struct RemoteEventContext<'a> {
56    pub event_id: &'a EventId,
57    pub raw_event: &'a Raw<AnySyncTimelineEvent>,
58    pub relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
59    pub bundled_edit_encryption_info: Option<Arc<EncryptionInfo>>,
60}
61
62#[derive(Clone, Debug)]
63pub(in crate::timeline) struct TimelineMetadata {
64    // **** CONSTANT FIELDS ****
65    /// An optional prefix for internal IDs, defined during construction of the
66    /// timeline.
67    ///
68    /// This value is constant over the lifetime of the metadata.
69    internal_id_prefix: Option<String>,
70
71    /// The `count` value for the `Skip` higher-order stream used by the
72    /// `TimelineSubscriber`. See its documentation to learn more.
73    pub(super) subscriber_skip_count: SkipCount,
74
75    /// The hook to call whenever we run into a unable-to-decrypt event.
76    ///
77    /// This value is constant over the lifetime of the metadata.
78    pub unable_to_decrypt_hook: Option<Arc<UtdHookManager>>,
79
80    /// A boolean indicating whether the room the timeline is attached to is
81    /// actually encrypted or not.
82    ///
83    /// May be false until we fetch the actual room encryption state.
84    pub is_room_encrypted: bool,
85
86    /// Rules of the version of the timeline's room, or a sensible default.
87    ///
88    /// This value is constant over the lifetime of the metadata.
89    pub room_version_rules: RoomVersionRules,
90
91    /// The own [`OwnedUserId`] of the client who opened the timeline.
92    pub(crate) own_user_id: OwnedUserId,
93
94    // **** DYNAMIC FIELDS ****
95    /// The next internal identifier for timeline items, used for both local and
96    /// remote echoes.
97    ///
98    /// This is never cleared, but always incremented, to avoid issues with
99    /// reusing a stale internal id across timeline clears. We don't expect
100    /// we can hit `u64::max_value()` realistically, but if this would
101    /// happen, we do a wrapping addition when incrementing this
102    /// id; the previous 0 value would have disappeared a long time ago, unless
103    /// the device has terabytes of RAM.
104    next_internal_id: u64,
105
106    /// Aggregation metadata and pending aggregations.
107    pub aggregations: Aggregations,
108
109    /// Given an event, what are all the events that are replies to it?
110    ///
111    /// Only works for remote events *and* replies which are remote-echoed.
112    pub replies: HashMap<OwnedEventId, BTreeSet<OwnedEventId>>,
113
114    /// Identifier of the fully-read event, helping knowing where to introduce
115    /// the read marker.
116    pub fully_read_event: Option<OwnedEventId>,
117
118    /// Whether we have a fully read-marker item in the timeline, that's up to
119    /// date with the room's read marker.
120    ///
121    /// This is false when:
122    /// - The fully-read marker points to an event that is not in the timeline,
123    /// - The fully-read marker item would be the last item in the timeline.
124    pub has_up_to_date_read_marker_item: bool,
125
126    /// Read receipts related state.
127    ///
128    /// TODO: move this over to the event cache (see also #3058).
129    pub(super) read_receipts: ReadReceiptsState,
130
131    /// The event ID of the active RtcNotification item that should have
132    /// active_members populated.
133    ///
134    /// There is no real link to the active room call and a rtc notification
135    /// event. For example there could be several rtc notifications events for
136    /// the same call, but we only want to have a single active call tile in
137    /// the timeline. We achieve this by keeping a link to the latest
138    /// notification event in the timeline and the `active_call` info will
139    /// be attached to it.
140    pub(crate) active_rtc_notification_event_id: Option<OwnedEventId>,
141
142    /// Current active call info for the room.
143    ///
144    /// This info is updated everytime the active call membership and intent
145    /// change. It is cached to be attached dynamically to the latest
146    /// RtcNotification event inserted in the timeline.
147    pub(crate) active_call: Option<ActiveCallInfo>,
148}
149
150impl TimelineMetadata {
151    pub(in crate::timeline) fn new(
152        own_user_id: OwnedUserId,
153        room_version_rules: RoomVersionRules,
154        internal_id_prefix: Option<String>,
155        unable_to_decrypt_hook: Option<Arc<UtdHookManager>>,
156        is_room_encrypted: bool,
157    ) -> Self {
158        Self {
159            subscriber_skip_count: SkipCount::new(),
160            own_user_id,
161            next_internal_id: Default::default(),
162            aggregations: Default::default(),
163            replies: Default::default(),
164            fully_read_event: Default::default(),
165            // It doesn't make sense to set this to false until we fill the `fully_read_event`
166            // field, otherwise we'll keep on exiting early in `Self::update_read_marker`.
167            has_up_to_date_read_marker_item: true,
168            read_receipts: Default::default(),
169            room_version_rules,
170            unable_to_decrypt_hook,
171            internal_id_prefix,
172            is_room_encrypted,
173            active_rtc_notification_event_id: None,
174            active_call: None,
175        }
176    }
177
178    pub(super) fn with_active_call_info(self, active_call_info: Option<ActiveCallInfo>) -> Self {
179        Self { active_call: active_call_info, ..self }
180    }
181
182    pub(super) fn clear(&mut self) {
183        // Note: we don't clear the next internal id to avoid bad cases of stale unique
184        // ids across timeline clears.
185        self.aggregations.clear();
186        self.replies.clear();
187        self.fully_read_event = None;
188        // We forgot about the fully read marker right above, so wait for a new one
189        // before attempting to update it for each new timeline item.
190        self.has_up_to_date_read_marker_item = true;
191        self.read_receipts.clear();
192    }
193
194    /// Get the relative positions of two events in the timeline.
195    ///
196    /// This method assumes that all events since the end of the timeline are
197    /// known.
198    ///
199    /// Returns `None` if none of the two events could be found in the timeline.
200    pub(in crate::timeline) fn compare_events_positions(
201        event_a: &EventId,
202        event_b: &EventId,
203        all_remote_events: &AllRemoteEvents,
204    ) -> Option<RelativePosition> {
205        if event_a == event_b {
206            return Some(RelativePosition::Same);
207        }
208
209        // We can make early returns here because we know all events since the end of
210        // the timeline, so the first event encountered is the oldest one.
211        for event_meta in all_remote_events.iter().rev() {
212            if event_meta.event_id == event_a {
213                return Some(RelativePosition::Before);
214            }
215            if event_meta.event_id == event_b {
216                return Some(RelativePosition::After);
217            }
218        }
219
220        None
221    }
222
223    /// Returns the next internal id for a timeline item (and increment our
224    /// internal counter).
225    fn next_internal_id(&mut self) -> TimelineUniqueId {
226        let val = self.next_internal_id;
227        self.next_internal_id = self.next_internal_id.wrapping_add(1);
228        let prefix = self.internal_id_prefix.as_deref().unwrap_or("");
229        TimelineUniqueId(format!("{prefix}{val}"))
230    }
231
232    /// Returns a new timeline item with a fresh internal id.
233    pub fn new_timeline_item(&mut self, kind: impl Into<TimelineItemKind>) -> Arc<TimelineItem> {
234        TimelineItem::new(kind, self.next_internal_id())
235    }
236
237    /// Returns a new timeline item reusing the recycled internal id, or with a
238    /// fresh internal id.
239    pub fn new_timeline_item_with_internal_id(
240        &mut self,
241        kind: impl Into<TimelineItemKind>,
242        recycled_timeline_id: Option<TimelineUniqueId>,
243    ) -> Arc<TimelineItem> {
244        TimelineItem::new(kind, recycled_timeline_id.unwrap_or_else(|| self.next_internal_id()))
245    }
246
247    /// Try to update the read marker item in the timeline.
248    pub(crate) fn update_read_marker(&mut self, items: &mut ObservableItemsTransaction<'_>) {
249        let Some(fully_read_event) = &self.fully_read_event else { return };
250        trace!(?fully_read_event, "Updating read marker");
251
252        let read_marker_idx = items
253            .iter_remotes_region()
254            .rev()
255            .find_map(|(idx, item)| item.is_read_marker().then_some(idx));
256
257        let mut fully_read_event_idx = items.iter_remotes_region().rev().find_map(|(idx, item)| {
258            (item.as_event()?.event_id() == Some(fully_read_event)).then_some(idx)
259        });
260
261        if let Some(fully_read_event_idx) = &mut fully_read_event_idx {
262            // The item at position `i` is the first item that's fully read, we're about to
263            // insert a read marker just after it.
264            //
265            // Do another forward pass to skip all the events we've sent too.
266
267            // Find the position of the first element…
268            let next = items
269                .iter_remotes_region()
270                // …strictly *after* the fully read event…
271                .skip_while(|(idx, _)| idx <= fully_read_event_idx)
272                // …that's not virtual and not sent by us…
273                .find_map(|(idx, item)| {
274                    (item.as_event()?.sender() != self.own_user_id).then_some(idx)
275                });
276
277            if let Some(next) = next {
278                // `next` point to the first item that's not sent by us, so the *previous* of
279                // next is the right place where to insert the fully read marker.
280                *fully_read_event_idx = next.wrapping_sub(1);
281            } else {
282                // There's no event after the read marker that's not sent by us, i.e. the full
283                // timeline has been read: the fully read marker goes to the end, even after the
284                // local timeline items.
285                //
286                // TODO (@hywan): Should we introduce a `items.position_of_last_remote()` to
287                // insert before the local timeline items?
288                *fully_read_event_idx = items.len().wrapping_sub(1);
289            }
290        }
291
292        match (read_marker_idx, fully_read_event_idx) {
293            (None, None) => {
294                // We didn't have a previous read marker, and we didn't find the fully-read
295                // event in the timeline items. Don't do anything, and retry on
296                // the next event we add.
297                self.has_up_to_date_read_marker_item = false;
298            }
299
300            (None, Some(idx)) => {
301                // Only insert the read marker if it is not at the end of the timeline.
302                if idx + 1 < items.len() {
303                    let idx = idx + 1;
304                    items.insert(idx, TimelineItem::read_marker(), None);
305                    self.has_up_to_date_read_marker_item = true;
306                } else {
307                    // The next event might require a read marker to be inserted at the current
308                    // end.
309                    self.has_up_to_date_read_marker_item = false;
310                }
311            }
312
313            (Some(_), None) => {
314                // We didn't find the timeline item containing the event referred to by the read
315                // marker. Retry next time we get a new event.
316                self.has_up_to_date_read_marker_item = false;
317            }
318
319            (Some(from), Some(to)) => {
320                if from >= to {
321                    // The read marker can't move backwards.
322                    if from + 1 == items.len() {
323                        // The read marker has nothing after it. An item disappeared; remove it.
324                        items.remove(from);
325                    }
326                    self.has_up_to_date_read_marker_item = true;
327                    return;
328                }
329
330                let prev_len = items.len();
331                let read_marker = items.remove(from);
332
333                // Only insert the read marker if it is not at the end of the timeline.
334                if to + 1 < prev_len {
335                    // Since the fully-read event's index was shifted to the left
336                    // by one position by the remove call above, insert the fully-
337                    // read marker at its previous position, rather than that + 1
338                    items.insert(to, read_marker, None);
339                    self.has_up_to_date_read_marker_item = true;
340                } else {
341                    self.has_up_to_date_read_marker_item = false;
342                }
343            }
344        }
345    }
346
347    /// Extract the content from a remote message-like event and process its
348    /// relations.
349    pub(crate) fn process_event_relations(
350        &mut self,
351        event: &AnySyncTimelineEvent,
352        raw_event: &Raw<AnySyncTimelineEvent>,
353        bundled_edit_encryption_info: Option<Arc<EncryptionInfo>>,
354        timeline_items: &Vector<Arc<TimelineItem>>,
355        is_thread_focus: bool,
356    ) -> (Option<InReplyToDetails>, Option<OwnedEventId>) {
357        if let AnySyncTimelineEvent::MessageLike(ev) = event
358            && let Some(content) = ev.original_content()
359        {
360            let remote_ctx = Some(RemoteEventContext {
361                event_id: ev.event_id(),
362                raw_event,
363                relations: ev.relations(),
364                bundled_edit_encryption_info,
365            });
366            self.process_content_relations(&content, remote_ctx, timeline_items, is_thread_focus)
367        } else {
368            (None, None)
369        }
370    }
371
372    /// Extracts the in-reply-to details and thread root from the content of a
373    /// message-like event, and take care of internal bookkeeping as well
374    /// (like marking responses).
375    ///
376    /// Returns the in-reply-to details and the thread root event ID, if any.
377    pub(crate) fn process_content_relations(
378        &mut self,
379        content: &AnyMessageLikeEventContent,
380        remote_ctx: Option<RemoteEventContext<'_>>,
381        timeline_items: &Vector<Arc<TimelineItem>>,
382        is_thread_focus: bool,
383    ) -> (Option<InReplyToDetails>, Option<OwnedEventId>) {
384        match content {
385            AnyMessageLikeEventContent::Sticker(content) => {
386                let (in_reply_to, thread_root) = Self::extract_reply_and_thread_root(
387                    content.relates_to.clone().and_then(|rel| rel.try_into().ok()),
388                    timeline_items,
389                    is_thread_focus,
390                );
391
392                if let Some(event_id) = remote_ctx.map(|ctx| ctx.event_id) {
393                    self.mark_response(event_id, in_reply_to.as_ref());
394                }
395
396                (in_reply_to, thread_root)
397            }
398
399            AnyMessageLikeEventContent::UnstablePollStart(UnstablePollStartEventContent::New(
400                c,
401            )) => {
402                let (in_reply_to, thread_root) = Self::extract_reply_and_thread_root(
403                    c.relates_to.clone(),
404                    timeline_items,
405                    is_thread_focus,
406                );
407
408                // Record the bundled edit in the aggregations set, if any.
409                if let Some(ctx) = remote_ctx {
410                    // Extract a potentially bundled edit.
411                    if let Some((edit_event_id, new_content)) =
412                        extract_poll_edit_content(ctx.relations)
413                    {
414                        let edit_json = extract_bundled_edit_event_json(ctx.raw_event);
415                        let aggregation = Aggregation::new(
416                            TimelineEventItemId::EventId(edit_event_id),
417                            AggregationKind::Edit(PendingEdit {
418                                kind: PendingEditKind::Poll(Replacement::new(
419                                    ctx.event_id.to_owned(),
420                                    new_content,
421                                )),
422                                edit_json,
423                                encryption_info: ctx.bundled_edit_encryption_info,
424                                bundled_item_owner: Some(ctx.event_id.to_owned()),
425                            }),
426                        );
427                        self.aggregations.add(
428                            TimelineEventItemId::EventId(ctx.event_id.to_owned()),
429                            aggregation,
430                        );
431                    }
432
433                    self.mark_response(ctx.event_id, in_reply_to.as_ref());
434                }
435
436                (in_reply_to, thread_root)
437            }
438
439            AnyMessageLikeEventContent::RoomMessage(msg) => {
440                let (in_reply_to, thread_root) = Self::extract_reply_and_thread_root(
441                    msg.relates_to.clone().and_then(|rel| rel.try_into().ok()),
442                    timeline_items,
443                    is_thread_focus,
444                );
445
446                // Record the bundled edit in the aggregations set, if any.
447                if let Some(ctx) = remote_ctx {
448                    // Extract a potentially bundled edit.
449                    if let Some((edit_event_id, new_content)) =
450                        extract_room_msg_edit_content(ctx.relations)
451                    {
452                        let edit_json = extract_bundled_edit_event_json(ctx.raw_event);
453                        let aggregation = Aggregation::new(
454                            TimelineEventItemId::EventId(edit_event_id),
455                            AggregationKind::Edit(PendingEdit {
456                                kind: PendingEditKind::RoomMessage(Replacement::new(
457                                    ctx.event_id.to_owned(),
458                                    new_content,
459                                )),
460                                edit_json,
461                                encryption_info: ctx.bundled_edit_encryption_info,
462                                bundled_item_owner: Some(ctx.event_id.to_owned()),
463                            }),
464                        );
465                        self.aggregations.add(
466                            TimelineEventItemId::EventId(ctx.event_id.to_owned()),
467                            aggregation,
468                        );
469                    }
470
471                    self.mark_response(ctx.event_id, in_reply_to.as_ref());
472                }
473
474                (in_reply_to, thread_root)
475            }
476
477            AnyMessageLikeEventContent::RoomEncrypted(msg) => {
478                let (in_reply_to, thread_root) = Self::extract_reply_and_thread_root(
479                    msg.relates_to.clone().and_then(|rel| match rel {
480                        Relation::Reply(reply) => Some(RelationWithoutReplacement::Reply(reply)),
481                        Relation::Thread(thread) => {
482                            Some(RelationWithoutReplacement::Thread(thread))
483                        }
484                        _ => None,
485                    }),
486                    timeline_items,
487                    is_thread_focus,
488                );
489
490                if let Some(ctx) = remote_ctx {
491                    self.mark_response(ctx.event_id, in_reply_to.as_ref());
492                }
493
494                (in_reply_to, thread_root)
495            }
496
497            _ => (None, None),
498        }
499    }
500
501    /// Extracts the in-reply-to details and thread root from a relation, if
502    /// available.
503    fn extract_reply_and_thread_root(
504        relates_to: Option<RelationWithoutReplacement>,
505        timeline_items: &Vector<Arc<TimelineItem>>,
506        is_thread_focus: bool,
507    ) -> (Option<InReplyToDetails>, Option<OwnedEventId>) {
508        let mut thread_root = None;
509
510        let in_reply_to = relates_to.and_then(|relation| match relation {
511            RelationWithoutReplacement::Reply(reply) => {
512                Some(InReplyToDetails::new(reply.in_reply_to.event_id, timeline_items))
513            }
514            RelationWithoutReplacement::Thread(thread) => {
515                thread_root = Some(thread.event_id);
516
517                if is_thread_focus && thread.is_falling_back {
518                    // In general, a threaded event is marked as a response to the previous message
519                    // in the thread, to maintain backwards compatibility with clients not
520                    // supporting threads.
521                    //
522                    // But we can have actual replies to other in-thread events. The
523                    // `is_falling_back` bool helps distinguishing both use cases.
524                    //
525                    // If this timeline is thread-focused, we only mark non-falling-back replies as
526                    // actual in-thread replies.
527                    None
528                } else {
529                    thread.in_reply_to.map(|in_reply_to| {
530                        InReplyToDetails::new(in_reply_to.event_id, timeline_items)
531                    })
532                }
533            }
534            _ => None,
535        });
536
537        (in_reply_to, thread_root)
538    }
539
540    /// Mark a message as a response to another message, if it is a reply.
541    fn mark_response(&mut self, event_id: &EventId, in_reply_to: Option<&InReplyToDetails>) {
542        // If this message is a reply to another message, add an entry in the
543        // inverted mapping.
544        if let Some(replied_to_event_id) = in_reply_to.as_ref().map(|details| &details.event_id) {
545            // This is a reply! Add an entry.
546            self.replies
547                .entry(replied_to_event_id.to_owned())
548                .or_default()
549                .insert(event_id.to_owned());
550        }
551    }
552}
553
554/// Result of comparing events position in the timeline.
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556pub(in crate::timeline) enum RelativePosition {
557    /// Event B is after (more recent than) event A.
558    After,
559    /// They are the same event.
560    Same,
561    /// Event B is before (older than) event A.
562    Before,
563}
564
565/// Metadata about an event that needs to be kept in memory.
566#[derive(Debug, Clone)]
567pub(in crate::timeline) struct EventMeta {
568    /// The ID of the event.
569    pub event_id: OwnedEventId,
570
571    /// The sender of the event, if known.
572    pub sender: Option<OwnedUserId>,
573
574    /// If this event is part of a thread, this will contain its thread root
575    /// event id.
576    pub thread_root_id: Option<OwnedEventId>,
577
578    /// Whether the event is among the timeline items.
579    pub visible: bool,
580
581    /// Whether the event can show read receipts.
582    pub can_show_read_receipts: bool,
583
584    /// Foundation for the mapping between remote events to timeline items.
585    ///
586    /// Let's explain it. The events represent the first set and are stored in
587    /// [`ObservableItems::all_remote_events`], and the timeline
588    /// items represent the second set and are stored in
589    /// [`ObservableItems::items`].
590    ///
591    /// Each event is mapped to at most one timeline item:
592    ///
593    /// - `None` if the event isn't rendered in the timeline (e.g. some state
594    ///   events, or malformed events) or is rendered as a timeline item that
595    ///   attaches to or groups with another item, like reactions,
596    /// - `Some(_)` if the event is rendered in the timeline.
597    ///
598    /// This is neither a surjection nor an injection. Every timeline item may
599    /// not be attached to an event, for example with a virtual timeline item.
600    /// We can formulate other rules:
601    ///
602    /// - a timeline item that doesn't _move_ and that is represented by an
603    ///   event has a mapping to an event,
604    /// - a virtual timeline item has no mapping to an event.
605    ///
606    /// Imagine the following remote events:
607    ///
608    /// | index | remote events |
609    /// +-------+---------------+
610    /// | 0     | `$ev0`        |
611    /// | 1     | `$ev1`        |
612    /// | 2     | `$ev2`        |
613    /// | 3     | `$ev3`        |
614    /// | 4     | `$ev4`        |
615    /// | 5     | `$ev5`        |
616    ///
617    /// Once rendered in a timeline, it for example produces:
618    ///
619    /// | index | item              | related items        |
620    /// +-------+-------------------+----------------------+
621    /// | 0     | content of `$ev0` |                      |
622    /// | 1     | content of `$ev2` | reaction with `$ev4` |
623    /// | 2     | date divider      |                      |
624    /// | 3     | content of `$ev3` |                      |
625    /// | 4     | content of `$ev5` |                      |
626    ///
627    /// Note the date divider that is a virtual item. Also note `$ev4` which is
628    /// a reaction to `$ev2`. Finally note that `$ev1` is not rendered in
629    /// the timeline.
630    ///
631    /// The mapping between remote event index to timeline item index will look
632    /// like this:
633    ///
634    /// | remote event index | timeline item index | comment                                    |
635    /// +--------------------+---------------------+--------------------------------------------+
636    /// | 0                  | `Some(0)`           | `$ev0` is rendered as the #0 timeline item |
637    /// | 1                  | `None`              | `$ev1` isn't rendered in the timeline      |
638    /// | 2                  | `Some(1)`           | `$ev2` is rendered as the #1 timeline item |
639    /// | 3                  | `Some(3)`           | `$ev3` is rendered as the #3 timeline item |
640    /// | 4                  | `None`              | `$ev4` is a reaction to item #1            |
641    /// | 5                  | `Some(4)`           | `$ev5` is rendered as the #4 timeline item |
642    ///
643    /// Note that the #2 timeline item (the day divider) doesn't map to any
644    /// remote event, but if it moves, it has an impact on this mapping.
645    pub timeline_item_index: Option<usize>,
646}
647
648impl EventMeta {
649    pub fn new(
650        event_id: OwnedEventId,
651        sender: Option<&UserId>,
652        visible: bool,
653        can_show_read_receipts: bool,
654        thread_root_id: Option<OwnedEventId>,
655    ) -> Self {
656        Self {
657            event_id,
658            sender: sender.map(ToOwned::to_owned),
659            thread_root_id,
660            visible,
661            can_show_read_receipts,
662            timeline_item_index: None,
663        }
664    }
665}