1use 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
71pub(super) enum Flow {
73 Local {
75 txn_id: OwnedTransactionId,
77
78 send_handle: Option<SendHandle>,
80 },
81
82 Remote {
85 event_id: OwnedEventId,
87 txn_id: Option<OwnedTransactionId>,
90 raw_event: Raw<AnySyncTimelineEvent>,
92 position: TimelineItemPosition,
94 encryption_info: Option<Arc<EncryptionInfo>>,
96 },
97}
98
99impl Flow {
100 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 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 pub(super) forwarder: Option<OwnedUserId>,
122 pub(super) forwarder_profile: Option<Profile>,
127 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 pub(super) should_add_new_items: bool,
139}
140
141#[derive(Clone, Debug)]
144pub(super) enum HandleAggregationKind {
145 Reaction { key: String },
147
148 Redaction,
150
151 Edit { replacement: Replacement<RoomMessageEventContentWithoutRelation> },
153
154 PollResponse { answers: Vec<String> },
156
157 PollEdit { replacement: Replacement<NewUnstablePollStartEventContentWithoutRelation> },
159
160 PollEnd,
162
163 BeaconUpdate { location: BeaconInfo },
165
166 BeaconStop { own_id: TimelineEventItemId, content: BeaconInfoEventContent },
172
173 CallDeclined,
175}
176
177impl HandleAggregationKind {
178 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#[derive(Clone, Debug)]
196#[allow(clippy::large_enum_variant)]
197pub(super) enum TimelineAction {
198 AddItem {
204 content: TimelineItemContent,
206 },
207
208 HandleAggregation {
214 related_event: OwnedEventId,
216 kind: HandleAggregationKind,
218 },
219}
220
221impl TimelineAction {
222 fn add_item(content: TimelineItemContent) -> Self {
224 Self::AddItem { content }
225 }
226
227 #[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 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 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 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 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 let event_id = ev.event_id.clone();
381 vec![Self::HandleAggregation {
382 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 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 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, },
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#[derive(Clone, Copy, Debug)]
563pub(super) enum TimelineItemPosition {
564 Start {
567 origin: RemoteEventOrigin,
569 },
570
571 End {
574 origin: RemoteEventOrigin,
576 },
577
578 At {
580 event_index: usize,
582
583 origin: RemoteEventOrigin,
585 },
586
587 UpdateAt {
592 timeline_item_index: usize,
594 },
595}
596
597pub(super) type RemovedItem = bool;
599
600pub(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 #[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 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 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 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 #[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 #[instrument(skip(self, content))]
840 fn handle_beacon_stop(&mut self, own_id: TimelineEventItemId, content: BeaconInfoEventContent) {
841 let sender = &self.ctx.sender;
842
843 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 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 #[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 #[instrument(skip_all, fields(redacts_event_id = ?redacted))]
896 fn handle_redaction(&mut self, redacted: OwnedEventId) {
897 if self.handle_aggregation_redaction(redacted.clone()) {
902 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 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 #[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 Err(err) => {
944 warn!("error while attempting to remove aggregation: {err}");
945 true
947 }
948 }
949 }
950
951 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 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 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 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 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 .map(|timeline_item_index| timeline_item_index + 1);
1113
1114 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 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 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 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 self.items.first_remotes_region_index()
1171 });
1172
1173 let event_index = self
1174 .items
1175 .all_remote_events()
1176 .last_index()
1177 .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 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 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 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 if is_rtc_notification {
1244 self.apply_active_call_to_last_rtc_notification();
1245 }
1246
1247 if !self.meta.has_up_to_date_read_marker_item {
1249 self.meta.update_read_marker(self.items);
1250 }
1251 }
1252
1253 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 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 return;
1268 }
1269
1270 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 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 if self.meta.active_call.is_some() {
1299 let TimelineItemContent::RtcNotification { call_intent, declined_by, .. } =
1300 last_notification.content()
1301 else {
1302 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 self.meta.active_rtc_notification_event_id =
1323 last_notification.event_id().map(ToOwned::to_owned);
1324 }
1325 }
1326 }
1327
1328 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 if let Some((local_timeline_item_index, local_timeline_item)) = items
1343 .iter_locals_region()
1345 .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 Some((nth, event_timeline_item))
1356 } else {
1357 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 let recycled = items.remove(local_timeline_item_index);
1374 TimelineItem::new(new_item, recycled.internal_id.clone())
1375 } else {
1376 meta.new_timeline_item_with_internal_id(new_item, recycled_timeline_id)
1378 }
1379 }
1380
1381 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
1428fn 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}