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}