Skip to main content

matrix_sdk_ui/timeline/
event_handler.rs

1// Copyright 2022 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::{borrow::Cow, sync::Arc};
16
17use as_variant::as_variant;
18use indexmap::IndexMap;
19use matrix_sdk::{
20    deserialized_responses::{EncryptionInfo, UnableToDecryptInfo},
21    send_queue::SendHandle,
22};
23use matrix_sdk_base::crypto::types::events::UtdCause;
24use ruma::{
25    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId,
26    TransactionId,
27    events::{
28        AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncStateEvent,
29        AnySyncTimelineEvent, MessageLikeEventContent, MessageLikeEventType,
30        StateEventContentChange, StateEventType, SyncStateEvent,
31        beacon_info::BeaconInfoEventContent,
32        poll::unstable_start::{
33            NewUnstablePollStartEventContentWithoutRelation, UnstablePollStartEventContent,
34        },
35        receipt::Receipt,
36        relation::Replacement,
37        room::message::{
38            Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
39        },
40    },
41    serde::Raw,
42};
43use tracing::{debug, error, field::debug, instrument, trace, warn};
44
45use super::{
46    BeaconInfo, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails,
47    LiveLocationState, MsgLikeContent, MsgLikeKind, OtherState, Sticker, ThreadSummary,
48    TimelineDetails, TimelineItem, TimelineItemContent,
49    controller::{
50        Aggregation, AggregationKind, ObservableItemsTransaction, PendingEditKind,
51        TimelineMetadata, TimelineStateTransaction, find_item_and_apply_aggregation,
52    },
53    date_dividers::DateDividerAdjuster,
54    event_item::{
55        AnyOtherStateEventContentChange, EventSendState, EventTimelineItemKind,
56        LocalEventTimelineItem, PollState, Profile, RemoteEventOrigin, RemoteEventTimelineItem,
57        TimelineEventItemId,
58    },
59    traits::RoomDataProvider,
60};
61use crate::{
62    timeline::{
63        TimelineUniqueId,
64        algorithms::rfind_event_item,
65        controller::aggregations::{AggregationSendHandle, PendingEdit},
66        event_item::OtherMessageLike,
67    },
68    unable_to_decrypt_hook::UtdHookManager,
69};
70
71/// When adding an event, useful information related to the source of the event.
72pub(super) enum Flow {
73    /// The event was locally created.
74    Local {
75        /// The transaction id we've used in requests associated to this event.
76        txn_id: OwnedTransactionId,
77
78        /// A handle to manipulate this event.
79        send_handle: Option<SendHandle>,
80    },
81
82    /// The event has been received from a remote source (sync, pagination,
83    /// etc.). This can be a "remote echo".
84    Remote {
85        /// The event identifier as returned by the server.
86        event_id: OwnedEventId,
87        /// The transaction id we might have used, if we're the sender of the
88        /// event.
89        txn_id: Option<OwnedTransactionId>,
90        /// The raw serialized JSON event.
91        raw_event: Raw<AnySyncTimelineEvent>,
92        /// Where should this be added in the timeline.
93        position: TimelineItemPosition,
94        /// Information about the encryption for this event.
95        encryption_info: Option<Arc<EncryptionInfo>>,
96    },
97}
98
99impl Flow {
100    /// Returns the [`TimelineEventItemId`] associated to this future item.
101    pub(crate) fn timeline_item_id(&self) -> TimelineEventItemId {
102        match self {
103            Flow::Remote { event_id, .. } => TimelineEventItemId::EventId(event_id.clone()),
104            Flow::Local { txn_id, .. } => TimelineEventItemId::TransactionId(txn_id.clone()),
105        }
106    }
107
108    /// If the flow is remote, returns the associated full raw event.
109    pub(crate) fn raw_event(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
110        as_variant!(self, Flow::Remote { raw_event, .. } => raw_event)
111    }
112}
113
114pub(super) struct TimelineEventContext {
115    pub(super) sender: OwnedUserId,
116    pub(super) sender_profile: Option<Profile>,
117    /// If the keys used to decrypt this event were shared-on-invite as part of
118    /// an [MSC4268] key bundle, the user ID of the forwarder.
119    ///
120    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
121    pub(super) forwarder: Option<OwnedUserId>,
122    /// If the keys used to decrypt this event were shared-on-invite as part of
123    /// an [MSC4268] key bundle, the forwarder's profile.
124    ///
125    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
126    pub(super) forwarder_profile: Option<Profile>,
127    /// The event's `origin_server_ts` field (or creation time for local echo).
128    pub(super) timestamp: MilliSecondsSinceUnixEpoch,
129    pub(super) read_receipts: IndexMap<OwnedUserId, Receipt>,
130    pub(super) is_highlighted: bool,
131    pub(super) flow: Flow,
132
133    /// If the event represents a new item, should it be added to the timeline?
134    ///
135    /// This controls whether a new timeline *may* be added. If the update kind
136    /// is about an update to an existing timeline item (redaction, edit,
137    /// reaction, etc.), it's always handled by default.
138    pub(super) should_add_new_items: bool,
139}
140
141/// Which kind of aggregation (i.e. modification of a related event) are we
142/// going to handle?
143#[derive(Clone, Debug)]
144pub(super) enum HandleAggregationKind {
145    /// Adding a reaction to the related event.
146    Reaction { key: String },
147
148    /// Redacting (removing) the related event.
149    Redaction,
150
151    /// Editing (replacing) the related event with another one.
152    Edit { replacement: Replacement<RoomMessageEventContentWithoutRelation> },
153
154    /// Responding to the related poll event.
155    PollResponse { answers: Vec<String> },
156
157    /// Editing a related poll event's description.
158    PollEdit { replacement: Replacement<NewUnstablePollStartEventContentWithoutRelation> },
159
160    /// Ending a related poll.
161    PollEnd,
162
163    /// A location update for a live location sharing session (MSC3489).
164    BeaconUpdate { location: BeaconInfo },
165
166    /// A stop event for a live location sharing session (MSC3489).
167    ///
168    /// Sent when the user stops sharing their location. Unlike [`BeaconUpdate`]
169    /// this does not carry a `relates_to` event ID; instead the target live
170    /// item is found by matching the sender.
171    BeaconStop { own_id: TimelineEventItemId, content: BeaconInfoEventContent },
172
173    /// A decline for an `m.rtc.notification` call.
174    CallDeclined,
175}
176
177impl HandleAggregationKind {
178    /// Returns a small string describing this aggregation, for debug purposes.
179    pub fn debug_string(&self) -> &'static str {
180        match self {
181            HandleAggregationKind::Reaction { .. } => "a reaction",
182            HandleAggregationKind::Redaction => "a redaction",
183            HandleAggregationKind::Edit { .. } => "an edit",
184            HandleAggregationKind::PollResponse { .. } => "a poll response",
185            HandleAggregationKind::PollEdit { .. } => "a poll edit",
186            HandleAggregationKind::PollEnd => "a poll end",
187            HandleAggregationKind::BeaconUpdate { .. } => "a beacon location update",
188            HandleAggregationKind::BeaconStop { .. } => "a beacon stop",
189            HandleAggregationKind::CallDeclined => "a call decline",
190        }
191    }
192}
193
194/// An action that we want to cause on the timeline.
195#[derive(Clone, Debug)]
196#[allow(clippy::large_enum_variant)]
197pub(super) enum TimelineAction {
198    /// Add a new timeline item.
199    ///
200    /// This enqueues adding a new item to the timeline (i.e. push to the items
201    /// array in its state). The item may be filtered out, and thus not
202    /// added later.
203    AddItem {
204        /// The content of the item we want to add.
205        content: TimelineItemContent,
206    },
207
208    /// Handle an aggregation to another event.
209    ///
210    /// The event the aggregation is related to might not be included in the
211    /// timeline, in which case it will be stashed somewhere, until we see
212    /// the related event.
213    HandleAggregation {
214        /// To which other event does this aggregation apply to?
215        related_event: OwnedEventId,
216        /// What kind of aggregation are we handling here?
217        kind: HandleAggregationKind,
218    },
219}
220
221impl TimelineAction {
222    /// Create a new [`TimelineEventKind::AddItem`].
223    fn add_item(content: TimelineItemContent) -> Self {
224        Self::AddItem { content }
225    }
226
227    /// Create all timeline actions from a given remote event.
228    ///
229    /// The return value may be empty if the event was a redacted reaction.
230    #[allow(clippy::too_many_arguments)]
231    pub async fn from_event<P: RoomDataProvider>(
232        event: AnySyncTimelineEvent,
233        raw_event: &Raw<AnySyncTimelineEvent>,
234        room_data_provider: &P,
235        unable_to_decrypt: Option<(UnableToDecryptInfo, Option<&Arc<UtdHookManager>>)>,
236        in_reply_to: Option<InReplyToDetails>,
237        thread_root: Option<OwnedEventId>,
238        thread_summary: Option<ThreadSummary>,
239    ) -> Vec<TimelineAction> {
240        let redaction_rules = room_data_provider.room_version_rules().redaction;
241
242        let redacted_message_or_none = |event_type: MessageLikeEventType| {
243            (event_type != MessageLikeEventType::Reaction).then_some(TimelineItemContent::MsgLike(
244                MsgLikeContent {
245                    thread_summary: thread_summary.clone(),
246                    ..MsgLikeContent::redacted()
247                },
248            ))
249        };
250
251        match event {
252            AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(ev)) => {
253                if let Some(redacts) = ev.redacts(&redaction_rules).map(ToOwned::to_owned) {
254                    vec![Self::HandleAggregation {
255                        related_event: redacts,
256                        kind: HandleAggregationKind::Redaction,
257                    }]
258                } else {
259                    redacted_message_or_none(ev.event_type())
260                        .map(Self::add_item)
261                        .into_iter()
262                        .collect()
263                }
264            }
265
266            AnySyncTimelineEvent::MessageLike(ev) => match ev.original_content() {
267                Some(AnyMessageLikeEventContent::RoomEncrypted(content)) => {
268                    // An event which is still encrypted.
269                    if let Some((unable_to_decrypt_info, unable_to_decrypt_hook_manager)) =
270                        unable_to_decrypt
271                    {
272                        let utd_cause = UtdCause::determine(
273                            raw_event,
274                            room_data_provider.crypto_context_info().await,
275                            &unable_to_decrypt_info,
276                        );
277
278                        // Let the hook know that we ran into an unable-to-decrypt that is added to
279                        // the timeline.
280                        if let Some(hook) = unable_to_decrypt_hook_manager {
281                            hook.on_utd(
282                                ev.event_id(),
283                                utd_cause,
284                                ev.origin_server_ts(),
285                                ev.sender(),
286                            )
287                            .await;
288                        }
289
290                        vec![Self::add_item(TimelineItemContent::MsgLike(
291                            MsgLikeContent::unable_to_decrypt(EncryptedMessage::from_content(
292                                content, utd_cause,
293                            )),
294                        ))]
295                    } else {
296                        // If we get here, it means that some part of the code has created a
297                        // `TimelineEvent` containing an `m.room.encrypted` event without
298                        // decrypting it. Possibly this means that encryption has not been
299                        // configured. We treat it the same as any other message-like event.
300                        vec![Self::from_content(
301                            AnyMessageLikeEventContent::RoomEncrypted(content),
302                            in_reply_to,
303                            thread_root,
304                            thread_summary,
305                        )]
306                    }
307                }
308
309                Some(content) => {
310                    vec![Self::from_content(content, in_reply_to, thread_root, thread_summary)]
311                }
312
313                None => redacted_message_or_none(ev.event_type())
314                    .map(Self::add_item)
315                    .into_iter()
316                    .collect(),
317            },
318
319            AnySyncTimelineEvent::State(ev) => match ev {
320                AnySyncStateEvent::RoomMember(ev) => match ev {
321                    SyncStateEvent::Original(ev) => {
322                        vec![Self::add_item(TimelineItemContent::room_member(
323                            ev.state_key,
324                            StateEventContentChange::Original {
325                                content: ev.content,
326                                prev_content: ev.unsigned.prev_content,
327                            },
328                            ev.sender,
329                        ))]
330                    }
331                    SyncStateEvent::Redacted(ev) => {
332                        vec![Self::add_item(TimelineItemContent::room_member(
333                            ev.state_key,
334                            StateEventContentChange::Redacted(ev.content),
335                            ev.sender,
336                        ))]
337                    }
338                },
339                AnySyncStateEvent::BeaconInfo(ev) => match ev {
340                    SyncStateEvent::Original(ev) => {
341                        // Check the `live` field directly, not `is_live()` which
342                        // considers timeout. We want to create a timeline item for any
343                        // beacon_info that was started as live, regardless of whether
344                        // the timeout has since expired.
345                        if ev.content.live {
346                            let add_item_action =
347                                Self::add_item(TimelineItemContent::MsgLike(MsgLikeContent {
348                                    kind: MsgLikeKind::LiveLocation(LiveLocationState::new(
349                                        ev.content,
350                                    )),
351                                    reactions: Default::default(),
352                                    thread_root: None,
353                                    in_reply_to: None,
354                                    thread_summary: None,
355                                }));
356                            let handle_aggregation_action = ev.unsigned.prev_content.map(|prev| {
357                                let prev_content = BeaconInfoEventContent::new(
358                                    prev.description,
359                                    prev.timeout,
360                                    false,
361                                    prev.ts,
362                                );
363                                Self::HandleAggregation {
364                                    related_event: ev.event_id.clone(),
365                                    kind: HandleAggregationKind::BeaconStop {
366                                        own_id: TimelineEventItemId::TransactionId(
367                                            TransactionId::new(),
368                                        ),
369                                        content: prev_content,
370                                    },
371                                }
372                            });
373                            let mut actions = vec![add_item_action];
374                            actions.extend(handle_aggregation_action);
375                            actions
376                        } else {
377                            // A non-live beacon_info is a stop event: it should update the
378                            // existing live item from the same sender rather than creating a
379                            // new timeline item.
380                            let event_id = ev.event_id.clone();
381                            vec![Self::HandleAggregation {
382                                // There is no explicit relates_to on a beacon_info state event;
383                                // the target is identified by sender in handle_beacon_stop.
384                                related_event: event_id.clone(),
385                                kind: HandleAggregationKind::BeaconStop {
386                                    own_id: TimelineEventItemId::EventId(event_id),
387                                    content: ev.content,
388                                },
389                            }]
390                        }
391                    }
392                    SyncStateEvent::Redacted(_) => {
393                        vec![Self::add_item(TimelineItemContent::MsgLike(
394                            MsgLikeContent::redacted(),
395                        ))]
396                    }
397                },
398                ev => vec![Self::add_item(TimelineItemContent::OtherState(OtherState {
399                    state_key: ev.state_key().to_owned(),
400                    content: AnyOtherStateEventContentChange::with_event_content(
401                        ev.content_change(),
402                    ),
403                }))],
404            },
405        }
406    }
407
408    /// Create a new [`TimelineAction`] from a given event's content.
409    ///
410    /// This is applicable to both remote event (as this is called from
411    /// [`TimelineAction::from_event`]) or local events (for which we only have
412    /// the content).
413    ///
414    /// The return value may be `None` if handling the event (be it a new item
415    /// or an aggregation) is not supported for this event type.
416    pub(super) fn from_content(
417        content: AnyMessageLikeEventContent,
418        in_reply_to: Option<InReplyToDetails>,
419        thread_root: Option<OwnedEventId>,
420        thread_summary: Option<ThreadSummary>,
421    ) -> Self {
422        match content {
423            AnyMessageLikeEventContent::Reaction(c) => {
424                // This is a reaction to a message.
425                Self::HandleAggregation {
426                    related_event: c.relates_to.event_id.clone(),
427                    kind: HandleAggregationKind::Reaction { key: c.relates_to.key },
428                }
429            }
430
431            AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent {
432                relates_to: Some(Relation::Replacement(re)),
433                ..
434            }) => Self::HandleAggregation {
435                related_event: re.event_id.clone(),
436                kind: HandleAggregationKind::Edit { replacement: re },
437            },
438
439            AnyMessageLikeEventContent::UnstablePollStart(
440                UnstablePollStartEventContent::Replacement(re),
441            ) => Self::HandleAggregation {
442                related_event: re.relates_to.event_id.clone(),
443                kind: HandleAggregationKind::PollEdit { replacement: re.relates_to },
444            },
445
446            AnyMessageLikeEventContent::UnstablePollResponse(c) => Self::HandleAggregation {
447                related_event: c.relates_to.event_id,
448                kind: HandleAggregationKind::PollResponse { answers: c.poll_response.answers },
449            },
450
451            AnyMessageLikeEventContent::UnstablePollEnd(c) => Self::HandleAggregation {
452                related_event: c.relates_to.event_id,
453                kind: HandleAggregationKind::PollEnd,
454            },
455
456            AnyMessageLikeEventContent::CallInvite(_) => {
457                Self::add_item(TimelineItemContent::CallInvite)
458            }
459
460            AnyMessageLikeEventContent::RtcNotification(c) => {
461                Self::add_item(TimelineItemContent::RtcNotification {
462                    call_intent: c.call_intent,
463                    declined_by: Vec::new(),
464                    active_call_info: None,
465                })
466            }
467
468            AnyMessageLikeEventContent::RtcDecline(c) => Self::HandleAggregation {
469                related_event: c.relates_to.event_id,
470                kind: HandleAggregationKind::CallDeclined,
471            },
472
473            AnyMessageLikeEventContent::Sticker(content) => {
474                Self::add_item(TimelineItemContent::MsgLike(MsgLikeContent {
475                    kind: MsgLikeKind::Sticker(Sticker { content }),
476                    reactions: Default::default(),
477                    thread_root,
478                    in_reply_to,
479                    thread_summary,
480                }))
481            }
482
483            AnyMessageLikeEventContent::UnstablePollStart(UnstablePollStartEventContent::New(
484                c,
485            )) => {
486                let poll_state = PollState::new(c.poll_start, c.text);
487
488                Self::AddItem {
489                    content: TimelineItemContent::MsgLike(MsgLikeContent {
490                        kind: MsgLikeKind::Poll(poll_state),
491                        reactions: Default::default(),
492                        thread_root,
493                        in_reply_to,
494                        thread_summary,
495                    }),
496                }
497            }
498
499            AnyMessageLikeEventContent::RoomMessage(msg) => Self::AddItem {
500                content: TimelineItemContent::message(
501                    msg.msgtype,
502                    msg.mentions,
503                    Default::default(),
504                    thread_root,
505                    in_reply_to,
506                    thread_summary,
507                ),
508            },
509
510            AnyMessageLikeEventContent::Beacon(content) => Self::HandleAggregation {
511                related_event: content.relates_to.event_id,
512                kind: HandleAggregationKind::BeaconUpdate {
513                    location: BeaconInfo {
514                        geo_uri: content.location.uri,
515                        ts: content.ts,
516                        description: content.location.description,
517                        encryption_info: None, // Filled in later from the event context.
518                    },
519                },
520            },
521
522            event => {
523                let other = OtherMessageLike { event_type: event.event_type() };
524
525                Self::AddItem {
526                    content: TimelineItemContent::MsgLike(MsgLikeContent {
527                        kind: MsgLikeKind::Other(other),
528                        reactions: Default::default(),
529                        thread_root,
530                        in_reply_to,
531                        thread_summary,
532                    }),
533                }
534            }
535        }
536    }
537
538    pub(super) fn failed_to_parse(event: FailedToParseEvent, error: serde_json::Error) -> Self {
539        let error = Arc::new(error);
540        match event {
541            FailedToParseEvent::State { event_type, state_key } => {
542                Self::add_item(TimelineItemContent::FailedToParseState {
543                    event_type,
544                    state_key,
545                    error,
546                })
547            }
548            FailedToParseEvent::MsgLike(event_type) => {
549                Self::add_item(TimelineItemContent::FailedToParseMessageLike { event_type, error })
550            }
551        }
552    }
553}
554
555#[derive(Debug)]
556pub(super) enum FailedToParseEvent {
557    MsgLike(MessageLikeEventType),
558    State { event_type: StateEventType, state_key: String },
559}
560
561/// The position at which to perform an update of the timeline with events.
562#[derive(Clone, Copy, Debug)]
563pub(super) enum TimelineItemPosition {
564    /// One or more items are prepended to the timeline (i.e. they're the
565    /// oldest).
566    Start {
567        /// The origin of the new item(s).
568        origin: RemoteEventOrigin,
569    },
570
571    /// One or more items are appended to the timeline (i.e. they're the most
572    /// recent).
573    End {
574        /// The origin of the new item(s).
575        origin: RemoteEventOrigin,
576    },
577
578    /// One item is inserted to the timeline.
579    At {
580        /// Where to insert the remote event.
581        event_index: usize,
582
583        /// The origin of the new item.
584        origin: RemoteEventOrigin,
585    },
586
587    /// A single item is updated.
588    ///
589    /// This can happen for instance after a UTD has been successfully
590    /// decrypted, or when it's been redacted at the source.
591    UpdateAt {
592        /// The index of the **timeline item**.
593        timeline_item_index: usize,
594    },
595}
596
597/// Whether an item was removed or not.
598pub(super) type RemovedItem = bool;
599
600/// Data necessary to update the timeline, given a single event to handle.
601///
602/// Bundles together a few things that are needed throughout the different
603/// stages of handling an event (figuring out whether it should update an
604/// existing timeline item, transforming that item or creating a new one,
605/// updating the reactive Vec).
606pub(super) struct TimelineEventHandler<'a, 'o> {
607    items: &'a mut ObservableItemsTransaction<'o>,
608    meta: &'a mut TimelineMetadata,
609    ctx: &'a TimelineEventContext,
610}
611
612impl<'a, 'o> TimelineEventHandler<'a, 'o> {
613    pub(super) fn new<P: RoomDataProvider>(
614        state: &'a mut TimelineStateTransaction<'o, P>,
615        ctx: &'a TimelineEventContext,
616    ) -> Self {
617        let TimelineStateTransaction { items, meta, .. } = state;
618        Self { items, meta, ctx }
619    }
620
621    /// Handle an event.
622    ///
623    /// Returns if an item was added to the timeline due to the new timeline
624    /// action. Items might not be added to the timeline for various reasons,
625    /// some common ones are if the item:
626    ///     - Contains an unsupported event type.
627    ///     - Is an edit or a redaction.
628    ///     - Contains a local echo turning into a remote echo.
629    ///     - Contains a message that is already in the timeline but was now
630    ///       decrypted.
631    ///
632    /// `raw_event` is only needed to determine the cause of any UTDs,
633    /// so if we know this is not a UTD it can be None.
634    #[instrument(skip_all, fields(txn_id, event_id, position))]
635    pub(super) async fn handle_event(
636        mut self,
637        date_divider_adjuster: &mut DateDividerAdjuster,
638        timeline_action: TimelineAction,
639        recycled_timeline_id: Option<TimelineUniqueId>,
640    ) -> bool {
641        let span = tracing::Span::current();
642
643        date_divider_adjuster.mark_used();
644
645        match &self.ctx.flow {
646            Flow::Local { txn_id, .. } => {
647                span.record("txn_id", debug(txn_id));
648                debug!("Handling local event");
649            }
650
651            Flow::Remote { event_id, txn_id, position, .. } => {
652                span.record("event_id", debug(event_id));
653                span.record("position", debug(position));
654                if let Some(txn_id) = txn_id {
655                    span.record("txn_id", debug(txn_id));
656                }
657                trace!("Handling remote event");
658            }
659        }
660
661        self.handle_timeline_action(timeline_action, recycled_timeline_id)
662    }
663
664    fn handle_timeline_action(
665        &mut self,
666        timeline_action: TimelineAction,
667        recycled_timeline_id: Option<TimelineUniqueId>,
668    ) -> bool {
669        match timeline_action {
670            TimelineAction::AddItem { content } => {
671                if self.ctx.should_add_new_items {
672                    self.add_item(content, recycled_timeline_id);
673                    true
674                } else {
675                    false
676                }
677            }
678            TimelineAction::HandleAggregation { related_event, kind } => {
679                self.handle_aggregation_action(related_event, kind);
680                false
681            }
682        }
683    }
684
685    fn handle_aggregation_action(
686        &mut self,
687        related_event: OwnedEventId,
688        kind: HandleAggregationKind,
689    ) {
690        match kind {
691            HandleAggregationKind::Reaction { key } => {
692                self.handle_reaction(related_event, key);
693            }
694            HandleAggregationKind::Redaction => {
695                self.handle_redaction(related_event);
696            }
697            HandleAggregationKind::Edit { replacement } => {
698                self.handle_edit(
699                    replacement.event_id.clone(),
700                    PendingEditKind::RoomMessage(replacement),
701                );
702            }
703            HandleAggregationKind::PollResponse { answers } => {
704                self.handle_poll_response(related_event, answers);
705            }
706            HandleAggregationKind::PollEdit { replacement } => {
707                self.handle_edit(replacement.event_id.clone(), PendingEditKind::Poll(replacement));
708            }
709            HandleAggregationKind::PollEnd => {
710                self.handle_poll_end(related_event);
711            }
712            HandleAggregationKind::BeaconUpdate { mut location } => {
713                // Propagate the encryption info from the event context into the
714                // beacon location update so it can be inspected later.
715                let encryption_info = as_variant!(
716                    &self.ctx.flow,
717                    Flow::Remote { encryption_info, .. } => encryption_info.clone()
718                )
719                .flatten();
720                location.encryption_info = encryption_info;
721                self.handle_beacon_update(related_event, location);
722            }
723            HandleAggregationKind::BeaconStop { own_id, content } => {
724                self.handle_beacon_stop(own_id, content);
725            }
726            HandleAggregationKind::CallDeclined => {
727                self.handle_call_declined(related_event);
728            }
729        }
730    }
731
732    /// Build an aggregation owned by the event being handled: a pending local
733    /// echo when the flow is local, a remote one otherwise.
734    fn new_aggregation(&self, kind: AggregationKind) -> Aggregation {
735        let own_id = self.ctx.flow.timeline_item_id();
736        match &self.ctx.flow {
737            Flow::Local { send_handle, .. } => Aggregation::new_local(
738                own_id,
739                kind,
740                send_handle.clone().map(AggregationSendHandle::Event),
741            ),
742            Flow::Remote { .. } => Aggregation::new(own_id, kind),
743        }
744    }
745
746    #[instrument(skip(self, edit_kind))]
747    fn handle_edit(&mut self, edited_event_id: OwnedEventId, edit_kind: PendingEditKind) {
748        let target = TimelineEventItemId::EventId(edited_event_id.clone());
749
750        let encryption_info =
751            as_variant!(&self.ctx.flow, Flow::Remote { encryption_info, .. } => encryption_info.clone()).flatten();
752        let aggregation = self.new_aggregation(AggregationKind::Edit(PendingEdit {
753            kind: edit_kind,
754            edit_json: self.ctx.flow.raw_event().cloned(),
755            encryption_info,
756            bundled_item_owner: None,
757        }));
758
759        self.meta.aggregations.add(target.clone(), aggregation.clone());
760
761        if let Some(new_item) = find_item_and_apply_aggregation(
762            &self.meta.aggregations,
763            self.items,
764            &target,
765            aggregation,
766            &self.meta.room_version_rules,
767        ) {
768            // Update all events that replied to this message with the edited content.
769            Self::maybe_update_responses(
770                self.meta,
771                self.items,
772                &edited_event_id,
773                EmbeddedEvent::from_timeline_item(&new_item),
774            );
775        }
776    }
777
778    /// Apply a reaction to a *remote* event.
779    ///
780    /// Reactions to local events are applied in
781    /// [`crate::timeline::TimelineController::handle_local_echo`].
782    #[instrument(skip(self))]
783    fn handle_reaction(&mut self, relates_to: OwnedEventId, reaction_key: String) {
784        let target = TimelineEventItemId::EventId(relates_to);
785
786        let aggregation = self.new_aggregation(AggregationKind::Reaction {
787            key: reaction_key,
788            sender: self.ctx.sender.clone(),
789            timestamp: self.ctx.timestamp,
790        });
791
792        self.meta.aggregations.add(target.clone(), aggregation.clone());
793        find_item_and_apply_aggregation(
794            &self.meta.aggregations,
795            self.items,
796            &target,
797            aggregation,
798            &self.meta.room_version_rules,
799        );
800    }
801
802    fn handle_poll_response(&mut self, poll_event_id: OwnedEventId, answers: Vec<String>) {
803        let target = TimelineEventItemId::EventId(poll_event_id);
804        let aggregation = self.new_aggregation(AggregationKind::PollResponse {
805            sender: self.ctx.sender.clone(),
806            timestamp: self.ctx.timestamp,
807            answers,
808        });
809        self.meta.aggregations.add(target.clone(), aggregation.clone());
810        find_item_and_apply_aggregation(
811            &self.meta.aggregations,
812            self.items,
813            &target,
814            aggregation,
815            &self.meta.room_version_rules,
816        );
817    }
818
819    fn handle_poll_end(&mut self, poll_event_id: OwnedEventId) {
820        let target = TimelineEventItemId::EventId(poll_event_id);
821        let aggregation =
822            self.new_aggregation(AggregationKind::PollEnd { end_date: self.ctx.timestamp });
823        self.meta.aggregations.add(target.clone(), aggregation.clone());
824        find_item_and_apply_aggregation(
825            &self.meta.aggregations,
826            self.items,
827            &target,
828            aggregation,
829            &self.meta.room_version_rules,
830        );
831    }
832
833    /// Handle a stop `beacon_info` state event by finding the existing live
834    /// `LiveLocation` timeline item from the same sender and updating it via
835    /// the aggregation system.
836    ///
837    /// The stop event's content must match the start item's content (except for
838    /// the `live` field) to ensure we apply the stop to the correct session.
839    #[instrument(skip(self, content))]
840    fn handle_beacon_stop(&mut self, own_id: TimelineEventItemId, content: BeaconInfoEventContent) {
841        let sender = &self.ctx.sender;
842
843        // Find the live start item by sender and matching content.
844        let target_event_id = rfind_event_item(self.items, |item| {
845            item.sender() == sender
846                && item.content().as_live_location_state().is_some_and(|s| s.matches_stop(&content))
847        })
848        .and_then(|(_, event_item)| event_item.inner.event_id().map(ToOwned::to_owned));
849
850        let aggregation = Aggregation::new(own_id, AggregationKind::BeaconStop { content });
851
852        let Some(target_event_id) = target_event_id else {
853            // The live start item hasn't arrived yet (or the content doesn't match).
854            // Stash the stop so it can be applied when the matching start item arrives.
855            trace!(
856                "no matching live beacon_info item found for {sender}; \
857                 stashing stop event to apply when the start item arrives"
858            );
859            self.meta.aggregations.add_pending_beacon_stop(sender.clone(), aggregation);
860            return;
861        };
862
863        let target = TimelineEventItemId::EventId(target_event_id);
864        self.meta.aggregations.add(target.clone(), aggregation.clone());
865        find_item_and_apply_aggregation(
866            &self.meta.aggregations,
867            self.items,
868            &target,
869            aggregation,
870            &self.meta.room_version_rules,
871        );
872    }
873
874    /// Handle a location update from a beacon event aggregating onto the
875    /// related `beacon_info` state event's timeline item.
876    #[instrument(skip(self, location))]
877    fn handle_beacon_update(&mut self, beacon_info_event_id: OwnedEventId, location: BeaconInfo) {
878        let target = TimelineEventItemId::EventId(beacon_info_event_id);
879        let aggregation = self.new_aggregation(AggregationKind::BeaconUpdate { location });
880        self.meta.aggregations.add(target.clone(), aggregation.clone());
881        find_item_and_apply_aggregation(
882            &self.meta.aggregations,
883            self.items,
884            &target,
885            aggregation,
886            &self.meta.room_version_rules,
887        );
888    }
889
890    /// Looks for the redacted event in all the timeline event items, and
891    /// redacts it.
892    ///
893    /// This assumes the redacted event was present in the timeline in the first
894    /// place; it will warn if the redacted event has not been found.
895    #[instrument(skip_all, fields(redacts_event_id = ?redacted))]
896    fn handle_redaction(&mut self, redacted: OwnedEventId) {
897        // TODO: Apply local redaction of PollResponse and PollEnd events.
898        // https://github.com/matrix-org/matrix-rust-sdk/pull/2381#issuecomment-1689647825
899
900        // If it's an aggregation that's being redacted, handle it here.
901        if self.handle_aggregation_redaction(redacted.clone()) {
902            // When we have raw timeline items, we should not return here anymore, as we
903            // might need to redact the raw item as well.
904            return;
905        }
906
907        let target = TimelineEventItemId::EventId(redacted.clone());
908        let aggregation = self.new_aggregation(AggregationKind::Redaction);
909        self.meta.aggregations.add(target.clone(), aggregation.clone());
910
911        find_item_and_apply_aggregation(
912            &self.meta.aggregations,
913            self.items,
914            &target,
915            aggregation,
916            &self.meta.room_version_rules,
917        );
918
919        // Even if the redacted event wasn't in the timeline, we can always update
920        // responses with a placeholder "redacted" embedded item.
921        let embedded_event = EmbeddedEvent {
922            content: TimelineItemContent::MsgLike(MsgLikeContent::redacted()),
923            sender: self.ctx.sender.clone(),
924            sender_profile: TimelineDetails::from_initial_value(self.ctx.sender_profile.clone()),
925            timestamp: self.ctx.timestamp,
926            identifier: TimelineEventItemId::EventId(redacted.clone()),
927        };
928
929        Self::maybe_update_responses(self.meta, self.items, &redacted, embedded_event);
930    }
931
932    /// Attempts to redact an aggregation (e.g. a reaction, a poll response,
933    /// etc.).
934    ///
935    /// Returns true if it's succeeded.
936    #[instrument(skip_all, fields(redacts = ?aggregation_id))]
937    fn handle_aggregation_redaction(&mut self, aggregation_id: OwnedEventId) -> bool {
938        let aggregation_id = TimelineEventItemId::EventId(aggregation_id);
939
940        match self.meta.aggregations.try_remove_aggregation(&aggregation_id, self.items) {
941            Ok(val) => val,
942            // This wasn't a known aggregation that was redacted.
943            Err(err) => {
944                warn!("error while attempting to remove aggregation: {err}");
945                // It could find an aggregation but didn't properly unapply it.
946                true
947            }
948        }
949    }
950
951    /// Handle a call decline event by updating the related call notification
952    /// event and adding the new decliner to the list via the manager.
953    fn handle_call_declined(&mut self, notification_event_id: OwnedEventId) {
954        let target = TimelineEventItemId::EventId(notification_event_id);
955        let aggregation =
956            self.new_aggregation(AggregationKind::CallDeclined { sender: self.ctx.sender.clone() });
957        self.meta.aggregations.add(target.clone(), aggregation.clone());
958        find_item_and_apply_aggregation(
959            &self.meta.aggregations,
960            self.items,
961            &target,
962            aggregation,
963            &self.meta.room_version_rules,
964        );
965    }
966
967    /// Add a new event item in the timeline.
968    ///
969    /// # Safety
970    ///
971    /// This method is not marked as unsafe **but** it manipulates
972    /// [`ObservableItemsTransaction::all_remote_events`]. 2 rules **must** be
973    /// respected:
974    ///
975    /// 1. the remote event of the item being added **must** be present in
976    ///    `all_remote_events`,
977    /// 2. the lastly added or updated remote event must be associated to the
978    ///    timeline item being added here.
979    fn add_item(
980        &mut self,
981        content: TimelineItemContent,
982        recycled_timeline_id: Option<TimelineUniqueId>,
983    ) {
984        let sender = self.ctx.sender.to_owned();
985        let sender_profile = TimelineDetails::from_initial_value(self.ctx.sender_profile.clone());
986
987        let forwarder = self.ctx.forwarder.to_owned();
988        let forwarder_profile = self
989            .ctx
990            .forwarder
991            .as_ref()
992            .map(|_| TimelineDetails::from_initial_value(self.ctx.forwarder_profile.clone()));
993
994        let timestamp = self.ctx.timestamp;
995        let is_rtc_notification = matches!(content, TimelineItemContent::RtcNotification { .. });
996
997        let kind: EventTimelineItemKind = match &self.ctx.flow {
998            Flow::Local { txn_id, send_handle } => LocalEventTimelineItem {
999                send_state: EventSendState::NotSentYet { progress: None },
1000                transaction_id: txn_id.to_owned(),
1001                send_handle: send_handle.clone(),
1002            }
1003            .into(),
1004
1005            Flow::Remote { event_id, raw_event, position, txn_id, encryption_info, .. } => {
1006                let origin = match *position {
1007                    TimelineItemPosition::Start { origin }
1008                    | TimelineItemPosition::End { origin }
1009                    | TimelineItemPosition::At { origin, .. } => origin,
1010
1011                    // For updates, reuse the origin of the encrypted event.
1012                    TimelineItemPosition::UpdateAt { timeline_item_index: idx } => self.items[idx]
1013                        .as_event()
1014                        .and_then(|ev| Some(ev.as_remote()?.origin))
1015                        .unwrap_or_else(|| {
1016                            error!("Tried to update a local event");
1017                            RemoteEventOrigin::Unknown
1018                        }),
1019                };
1020
1021                RemoteEventTimelineItem {
1022                    event_id: event_id.clone(),
1023                    transaction_id: txn_id.clone(),
1024                    read_receipts: self.ctx.read_receipts.clone(),
1025                    is_own: self.ctx.sender == self.meta.own_user_id,
1026                    is_highlighted: self.ctx.is_highlighted,
1027                    encryption_info: encryption_info.clone(),
1028                    original_json: Some(raw_event.clone()),
1029                    latest_edit_json: None,
1030                    origin,
1031                }
1032                .into()
1033            }
1034        };
1035
1036        let is_room_encrypted = self.meta.is_room_encrypted;
1037
1038        let item = EventTimelineItem::new(
1039            sender,
1040            sender_profile,
1041            forwarder,
1042            forwarder_profile,
1043            timestamp,
1044            content,
1045            kind,
1046            is_room_encrypted,
1047        );
1048
1049        // Apply any pending or stashed aggregations.
1050        let mut cowed = Cow::Owned(item);
1051        if let Err(err) = self.meta.aggregations.apply_all(
1052            &self.ctx.flow.timeline_item_id(),
1053            &self.ctx.sender,
1054            &mut cowed,
1055            self.items,
1056            &self.meta.room_version_rules,
1057        ) {
1058            warn!("discarding aggregations: {err}");
1059        }
1060        let item = cowed.into_owned();
1061
1062        match &self.ctx.flow {
1063            Flow::Local { .. } => {
1064                trace!("Adding new local timeline item");
1065
1066                let item = self.meta.new_timeline_item_with_internal_id(item, recycled_timeline_id);
1067
1068                self.items.push_local(item);
1069            }
1070
1071            Flow::Remote {
1072                position: TimelineItemPosition::Start { .. }, event_id, txn_id, ..
1073            } => {
1074                let item = Self::recycle_local_or_create_item(
1075                    self.items,
1076                    self.meta,
1077                    item,
1078                    event_id,
1079                    txn_id.as_deref(),
1080                    recycled_timeline_id,
1081                );
1082
1083                trace!("Adding new remote timeline item at the start");
1084
1085                self.items.push_front(item, Some(0));
1086            }
1087
1088            Flow::Remote {
1089                position: TimelineItemPosition::At { event_index, .. },
1090                event_id,
1091                txn_id,
1092                ..
1093            } => {
1094                let item = Self::recycle_local_or_create_item(
1095                    self.items,
1096                    self.meta,
1097                    item,
1098                    event_id,
1099                    txn_id.as_deref(),
1100                    recycled_timeline_id,
1101                );
1102
1103                let all_remote_events = self.items.all_remote_events();
1104                let event_index = *event_index;
1105
1106                // Look for the closest `timeline_item_index` at the left of `event_index`.
1107                let timeline_item_index = all_remote_events
1108                    .range(0..=event_index)
1109                    .rev()
1110                    .find_map(|event_meta| event_meta.timeline_item_index)
1111                    // The new `timeline_item_index` is the previous + 1.
1112                    .map(|timeline_item_index| timeline_item_index + 1);
1113
1114                // No index? Look for the closest `timeline_item_index` at the right of
1115                // `event_index`.
1116                let timeline_item_index = timeline_item_index.or_else(|| {
1117                    all_remote_events
1118                        .range(event_index + 1..)
1119                        .find_map(|event_meta| event_meta.timeline_item_index)
1120                });
1121
1122                // Still no index? Well, it means there is no existing `timeline_item_index`
1123                // so we are inserting at the last non-local item position as a fallback.
1124                let timeline_item_index = timeline_item_index.unwrap_or_else(|| {
1125                    self.items
1126                        .iter_remotes_region()
1127                        .rev()
1128                        .find_map(|(timeline_item_index, timeline_item)| {
1129                            timeline_item.as_event().map(|_| timeline_item_index + 1)
1130                        })
1131                        .unwrap_or_else(|| {
1132                            // There is no remote timeline item, so we could insert at the start of
1133                            // the remotes region.
1134                            self.items.first_remotes_region_index()
1135                        })
1136                });
1137
1138                trace!(
1139                    ?event_index,
1140                    ?timeline_item_index,
1141                    "Adding new remote timeline at specific event index"
1142                );
1143
1144                self.items.insert(timeline_item_index, item, Some(event_index));
1145            }
1146
1147            Flow::Remote {
1148                position: TimelineItemPosition::End { .. }, event_id, txn_id, ..
1149            } => {
1150                let item = Self::recycle_local_or_create_item(
1151                    self.items,
1152                    self.meta,
1153                    item,
1154                    event_id,
1155                    txn_id.as_deref(),
1156                    recycled_timeline_id,
1157                );
1158
1159                // Let's find the latest remote event and insert after it
1160                let timeline_item_index = self
1161                    .items
1162                    .iter_remotes_region()
1163                    .rev()
1164                    .find_map(|(timeline_item_index, timeline_item)| {
1165                        timeline_item.as_event().map(|_| timeline_item_index + 1)
1166                    })
1167                    .unwrap_or_else(|| {
1168                        // There is no remote timeline item, so we could insert at the start of
1169                        // the remotes region.
1170                        self.items.first_remotes_region_index()
1171                    });
1172
1173                let event_index = self
1174                    .items
1175                    .all_remote_events()
1176                    .last_index()
1177                    // The last remote event is necessarily associated to this
1178                    // timeline item, see the contract of this method. Let's fallback to a similar
1179                    // value as `timeline_item_index` instead of panicking.
1180                    .or_else(|| {
1181                        error!(?event_id, "Failed to read the last event index from `AllRemoteEvents`: at least one event must be present");
1182
1183                        Some(0)
1184                    });
1185
1186                // Try to keep precise insertion semantics here, in this exact order:
1187                //
1188                // * _push back_ when the new item is inserted after all items (the assumption
1189                // being that this is the hot path, because most of the time new events
1190                // come from the sync),
1191                // * _push front_ when the new item is inserted at index 0,
1192                // * _insert_ otherwise.
1193
1194                if timeline_item_index == self.items.len() {
1195                    trace!("Adding new remote timeline item at the back");
1196                    self.items.push_back(item, event_index);
1197                } else if timeline_item_index == 0 {
1198                    trace!("Adding new remote timeline item at the front");
1199                    self.items.push_front(item, event_index);
1200                } else {
1201                    trace!(
1202                        timeline_item_index,
1203                        "Adding new remote timeline item at specific index"
1204                    );
1205                    self.items.insert(timeline_item_index, item, event_index);
1206                }
1207            }
1208
1209            Flow::Remote {
1210                event_id: decrypted_event_id,
1211                position: TimelineItemPosition::UpdateAt { timeline_item_index: idx },
1212                ..
1213            } => {
1214                // The event cache redacts the raw event independently of the aggregations
1215                // system, which reaches us here as an `UpdateAt`. If the item is already
1216                // redacted (via the aggregations system, applied earlier in the diff batch),
1217                // skip it to avoid a spurious duplicate update.
1218                let already_redacted = item.content().is_redacted()
1219                    && self.items[*idx]
1220                        .as_event()
1221                        .is_some_and(|existing| existing.content().is_redacted());
1222
1223                if already_redacted {
1224                    trace!("Item at position {idx} is already redacted, skipping the update");
1225                } else {
1226                    trace!("Updating timeline item at position {idx}");
1227
1228                    // Update all events that replied to this previously encrypted message.
1229                    Self::maybe_update_responses(
1230                        self.meta,
1231                        self.items,
1232                        decrypted_event_id,
1233                        EmbeddedEvent::from_timeline_item(&item),
1234                    );
1235
1236                    let internal_id = self.items[*idx].internal_id.clone();
1237                    self.items.replace(*idx, TimelineItem::new(item, internal_id));
1238                }
1239            }
1240        }
1241
1242        // Handle RTC notification active members population and cleanup
1243        if is_rtc_notification {
1244            self.apply_active_call_to_last_rtc_notification();
1245        }
1246
1247        // If we don't have a read marker item, look if we need to add one now.
1248        if !self.meta.has_up_to_date_read_marker_item {
1249            self.meta.update_read_marker(self.items);
1250        }
1251    }
1252
1253    /// Ensures that the last RtcNotification in the timeline has the current
1254    /// active call info, and cleans up any previous notification that had
1255    /// active members.
1256    fn apply_active_call_to_last_rtc_notification(&mut self) {
1257        let last_notification = rfind_event_item(self.items, |it| {
1258            matches!(it.content(), TimelineItemContent::RtcNotification { .. })
1259        })
1260        .map(|(a, b)| (a, b.internal_id.clone(), b.clone()));
1261
1262        if let Some((idx, internal_id, last_notification)) = last_notification {
1263            // Is this the same as previously?
1264            if let Some(prev_event_id) = &self.meta.active_rtc_notification_event_id {
1265                if Some(prev_event_id.as_ref()) == last_notification.event_id() {
1266                    // then no changes, the newest rtc_notification event have not change
1267                    return;
1268                }
1269
1270                // They are different, clean the old event
1271                // Find the previous notification item and clear its active_members
1272                if let Some((idx, prev_item)) =
1273                    rfind_event_item(self.items, |it: &EventTimelineItem| {
1274                        it.event_id() == Some(prev_event_id)
1275                    })
1276                    && let TimelineItemContent::RtcNotification {
1277                        call_intent,
1278                        declined_by,
1279                        active_call_info,
1280                    } = prev_item.content()
1281                {
1282                    // Only clear if it has active members (not already empty)
1283                    if active_call_info.is_some() {
1284                        let new_content = TimelineItemContent::RtcNotification {
1285                            call_intent: call_intent.clone(),
1286                            declined_by: declined_by.clone(),
1287                            active_call_info: None,
1288                        };
1289                        let new_event_item = prev_item.inner.with_content(new_content);
1290                        let new_timeline_item =
1291                            TimelineItem::new(new_event_item, prev_item.internal_id.clone());
1292                        self.items.replace(idx, new_timeline_item);
1293                    }
1294                }
1295            }
1296
1297            // Update the new last notification if needed
1298            if self.meta.active_call.is_some() {
1299                let TimelineItemContent::RtcNotification { call_intent, declined_by, .. } =
1300                    last_notification.content()
1301                else {
1302                    // Nothing to update
1303                    return;
1304                };
1305
1306                let new_content = TimelineItemContent::RtcNotification {
1307                    call_intent: call_intent.clone(),
1308                    declined_by: declined_by.clone(),
1309                    active_call_info: self
1310                        .meta
1311                        .active_call
1312                        .clone()
1313                        .map(|c| c.with_start_time(Some(self.ctx.timestamp))),
1314                };
1315
1316                let updated_event_item = last_notification.with_content(new_content);
1317                let new_timeline_item = TimelineItem::new(updated_event_item, internal_id);
1318                self.items.replace(idx, new_timeline_item);
1319
1320                // Update the active_rtc_notification_event_id so that this event gets any new
1321                // updates of call membership
1322                self.meta.active_rtc_notification_event_id =
1323                    last_notification.event_id().map(ToOwned::to_owned);
1324            }
1325        }
1326    }
1327
1328    /// Try to recycle a local timeline item for the same event, or create a new
1329    /// timeline item for it.
1330    ///
1331    /// Note: this method doesn't take `&mut self` to avoid a borrow checker
1332    /// conflict with `TimelineEventHandler::add_item`.
1333    fn recycle_local_or_create_item(
1334        items: &mut ObservableItemsTransaction<'_>,
1335        meta: &mut TimelineMetadata,
1336        mut new_item: EventTimelineItem,
1337        event_id: &EventId,
1338        transaction_id: Option<&TransactionId>,
1339        recycled_timeline_id: Option<TimelineUniqueId>,
1340    ) -> Arc<TimelineItem> {
1341        // Detect a local timeline item that matches `event_id` or `transaction_id`.
1342        if let Some((local_timeline_item_index, local_timeline_item)) = items
1343            // Iterate the locals region.
1344            .iter_locals_region()
1345            // Iterate from the end to the start.
1346            .rev()
1347            .find_map(|(nth, timeline_item)| {
1348                let event_timeline_item = timeline_item.as_event()?;
1349
1350                if Some(event_id) == event_timeline_item.event_id()
1351                    || (transaction_id.is_some()
1352                        && transaction_id == event_timeline_item.transaction_id())
1353                {
1354                    // A duplicate local event timeline item has been found!
1355                    Some((nth, event_timeline_item))
1356                } else {
1357                    // This local event timeline is not the one we are looking for. Continue our
1358                    // search.
1359                    None
1360                }
1361            })
1362        {
1363            trace!(
1364                ?event_id,
1365                ?transaction_id,
1366                ?local_timeline_item_index,
1367                "Removing local timeline item"
1368            );
1369
1370            transfer_details(&mut new_item, local_timeline_item);
1371
1372            // Remove the local timeline item.
1373            let recycled = items.remove(local_timeline_item_index);
1374            TimelineItem::new(new_item, recycled.internal_id.clone())
1375        } else {
1376            // We haven't found a matching local item to recycle; create a new item.
1377            meta.new_timeline_item_with_internal_id(new_item, recycled_timeline_id)
1378        }
1379    }
1380
1381    /// After updating the timeline item `new_item` which id is
1382    /// `target_event_id`, update other items that are responses to this item.
1383    fn maybe_update_responses(
1384        meta: &mut TimelineMetadata,
1385        items: &mut ObservableItemsTransaction<'_>,
1386        target_event_id: &EventId,
1387        new_embedded_event: EmbeddedEvent,
1388    ) {
1389        let Some(replies) = meta.replies.get(target_event_id) else {
1390            trace!("item has no replies");
1391            return;
1392        };
1393
1394        for reply_id in replies {
1395            let Some(timeline_item_index) = items
1396                .get_remote_event_by_event_id(reply_id)
1397                .and_then(|meta| meta.timeline_item_index)
1398            else {
1399                warn!(%reply_id, "event not known as an item in the timeline");
1400                continue;
1401            };
1402
1403            let Some(item) = items.get(timeline_item_index) else {
1404                warn!(%reply_id, timeline_item_index, "mapping from event id to timeline item likely incorrect");
1405                continue;
1406            };
1407
1408            let Some(event_item) = item.as_event() else { continue };
1409            let Some(msglike) = event_item.content.as_msglike() else { continue };
1410            let Some(message) = msglike.as_message() else { continue };
1411            let Some(in_reply_to) = msglike.in_reply_to.as_ref() else { continue };
1412
1413            trace!(reply_event_id = ?event_item.identifier(), "Updating response to updated event");
1414            let in_reply_to = InReplyToDetails {
1415                event_id: in_reply_to.event_id.clone(),
1416                event: TimelineDetails::Ready(Box::new(new_embedded_event.clone())),
1417            };
1418
1419            let new_reply_content = TimelineItemContent::MsgLike(
1420                msglike.with_in_reply_to(in_reply_to).with_kind(MsgLikeKind::Message(message)),
1421            );
1422            let new_reply_item = item.with_kind(event_item.with_content(new_reply_content));
1423            items.replace(timeline_item_index, new_reply_item);
1424        }
1425    }
1426}
1427
1428/// Transfer `TimelineDetails` that weren't available on the original
1429/// item and have been fetched separately (only `reply_to` for
1430/// now) from `old_item` to `item`, given two items for an event
1431/// that was re-received.
1432///
1433/// `old_item` *should* always be a local timeline item usually, but it
1434/// can be a remote timeline item.
1435fn transfer_details(new_item: &mut EventTimelineItem, old_item: &EventTimelineItem) {
1436    let TimelineItemContent::MsgLike(new_msglike) = &mut new_item.content else {
1437        return;
1438    };
1439    let TimelineItemContent::MsgLike(old_msglike) = &old_item.content else {
1440        return;
1441    };
1442
1443    let Some(in_reply_to) = &mut new_msglike.in_reply_to else { return };
1444    let Some(old_in_reply_to) = &old_msglike.in_reply_to else { return };
1445
1446    if matches!(&in_reply_to.event, TimelineDetails::Unavailable) {
1447        in_reply_to.event = old_in_reply_to.event.clone();
1448    }
1449}