1use std::{borrow::Cow, collections::HashMap, sync::Arc};
41
42use as_variant::as_variant;
43use matrix_sdk::{
44 check_validity_of_replacement_events,
45 deserialized_responses::EncryptionInfo,
46 send_queue::{RoomSendQueueStorageError, SendHandle, SendReactionHandle, SendRedactionHandle},
47};
48use ruma::{
49 MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, UserId,
50 events::{
51 AnySyncTimelineEvent, beacon_info::BeaconInfoEventContent,
52 poll::unstable_start::NewUnstablePollStartEventContentWithoutRelation,
53 relation::Replacement, room::message::RoomMessageEventContentWithoutRelation,
54 },
55 room_version_rules::RoomVersionRules,
56 serde::Raw,
57};
58use tracing::{error, info, trace, warn};
59
60use super::{ObservableItemsTransaction, rfind_event_by_item_id};
61use crate::timeline::{
62 BeaconInfo, EventSendState, EventTimelineItem, LiveLocationState, MsgLikeContent, MsgLikeKind,
63 PollState, ReactionInfo, TimelineEventItemId, TimelineItem, TimelineItemContent,
64 event_item::beacon_info_matches,
65};
66
67#[derive(Clone)]
68pub(in crate::timeline) enum PendingEditKind {
69 RoomMessage(Replacement<RoomMessageEventContentWithoutRelation>),
70 Poll(Replacement<NewUnstablePollStartEventContentWithoutRelation>),
71}
72
73impl std::fmt::Debug for PendingEditKind {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Self::RoomMessage(_) => f.debug_struct("RoomMessage").finish_non_exhaustive(),
77 Self::Poll(_) => f.debug_struct("Poll").finish_non_exhaustive(),
78 }
79 }
80}
81
82#[derive(Clone, Debug)]
83pub(in crate::timeline) struct PendingEdit {
84 pub kind: PendingEditKind,
86
87 pub edit_json: Option<Raw<AnySyncTimelineEvent>>,
89
90 pub encryption_info: Option<Arc<EncryptionInfo>>,
92
93 pub bundled_item_owner: Option<OwnedEventId>,
96}
97
98#[derive(Clone, Debug)]
100pub(crate) enum AggregationKind {
101 PollResponse {
103 sender: OwnedUserId,
105 timestamp: MilliSecondsSinceUnixEpoch,
107 answers: Vec<String>,
109 },
110
111 PollEnd {
113 end_date: MilliSecondsSinceUnixEpoch,
118 },
119
120 Reaction {
122 key: String,
124 sender: OwnedUserId,
126 timestamp: MilliSecondsSinceUnixEpoch,
128 },
129
130 Redaction,
135
136 Edit(PendingEdit),
144
145 BeaconUpdate { location: BeaconInfo },
147
148 BeaconStop { content: BeaconInfoEventContent },
156
157 CallDeclined {
159 sender: OwnedUserId,
161 },
162}
163
164#[derive(Clone, Debug)]
166pub(crate) enum AggregationSendHandle {
167 Event(SendHandle),
169 Reaction(SendReactionHandle),
171 Redaction(SendRedactionHandle),
173}
174
175impl AggregationSendHandle {
176 pub async fn abort(&self) -> Result<bool, RoomSendQueueStorageError> {
177 match self {
178 Self::Event(handle) => handle.abort().await,
179 Self::Reaction(handle) => handle.abort().await,
180 Self::Redaction(handle) => handle.abort().await,
181 }
182 }
183}
184
185#[derive(Clone, Debug)]
190pub(crate) struct Aggregation {
191 pub kind: AggregationKind,
193
194 pub own_id: TimelineEventItemId,
200
201 pub send_state: Option<EventSendState>,
204
205 pub send_handle: Option<AggregationSendHandle>,
207}
208
209fn poll_state_from_item<'a>(
211 event: &'a mut Cow<'_, EventTimelineItem>,
212) -> Result<&'a mut PollState, AggregationError> {
213 let content = event.to_mut().content_mut();
214
215 if let TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Poll(state), .. }) =
216 content
217 {
218 Ok(state)
219 } else {
220 Err(AggregationError::InvalidType {
221 expected: "a poll".to_owned(),
222 actual: content.debug_string().to_owned(),
223 })
224 }
225}
226
227fn live_location_state_from_item<'a>(
229 event: &'a mut Cow<'_, EventTimelineItem>,
230) -> Result<&'a mut LiveLocationState, AggregationError> {
231 let content = event.to_mut().content_mut();
232
233 if let TimelineItemContent::MsgLike(MsgLikeContent {
234 kind: MsgLikeKind::LiveLocation(state),
235 ..
236 }) = content
237 {
238 Ok(state)
239 } else {
240 Err(AggregationError::InvalidType {
241 expected: "a live location".to_owned(),
242 actual: content.debug_string().to_owned(),
243 })
244 }
245}
246
247fn rtc_notification_declinations_from_item<'a>(
249 event: &'a mut Cow<'_, EventTimelineItem>,
250) -> Result<&'a mut Vec<OwnedUserId>, AggregationError> {
251 let content = event.to_mut().content_mut();
252
253 if let TimelineItemContent::RtcNotification { declined_by, .. } = content {
254 Ok(declined_by)
255 } else {
256 Err(AggregationError::InvalidType {
257 expected: "an rtc notification".to_owned(),
258 actual: content.debug_string().to_owned(),
259 })
260 }
261}
262
263impl Aggregation {
264 pub fn new(own_id: TimelineEventItemId, kind: AggregationKind) -> Self {
266 Self { kind, own_id, send_state: None, send_handle: None }
267 }
268
269 pub fn new_local(
271 own_id: TimelineEventItemId,
272 kind: AggregationKind,
273 send_handle: Option<AggregationSendHandle>,
274 ) -> Self {
275 Self {
276 kind,
277 own_id,
278 send_state: Some(EventSendState::NotSentYet { progress: None }),
279 send_handle,
280 }
281 }
282
283 pub fn is_local(&self) -> bool {
285 !matches!(self.send_state, None | Some(EventSendState::Sent { .. }))
286 }
287
288 fn apply(
297 &self,
298 event: &mut Cow<'_, EventTimelineItem>,
299 rules: &RoomVersionRules,
300 ) -> ApplyAggregationResult {
301 match &self.kind {
302 AggregationKind::PollResponse { sender, timestamp, answers } => {
303 match poll_state_from_item(event) {
304 Ok(state) => {
305 state.add_response(sender.clone(), *timestamp, answers.clone());
306 ApplyAggregationResult::UpdatedItem
307 }
308 Err(err) => ApplyAggregationResult::Error(err),
309 }
310 }
311
312 AggregationKind::Redaction => {
313 let is_local = self.is_local();
314 let is_local_redacted =
315 event.content().is_redacted() && event.unredacted_item.is_some();
316 let is_remote_redacted =
317 event.content().is_redacted() && event.unredacted_item.is_none();
318 if is_local && is_local_redacted || !is_local && is_remote_redacted {
319 if event.redaction_send_state.is_some() && self.send_state.is_none() {
320 event.to_mut().redaction_send_state = None;
322 ApplyAggregationResult::UpdatedItem
323 } else {
324 ApplyAggregationResult::LeftItemIntact
325 }
326 } else {
327 let mut new_item = event.redact(&rules.redaction, is_local);
328 new_item.redaction_send_state = self.send_state.clone();
329 *event = Cow::Owned(new_item);
330 ApplyAggregationResult::UpdatedItem
331 }
332 }
333
334 AggregationKind::PollEnd { end_date } => match poll_state_from_item(event) {
335 Ok(state) => {
336 if !state.end(*end_date) {
337 return ApplyAggregationResult::Error(AggregationError::PollAlreadyEnded);
338 }
339 ApplyAggregationResult::UpdatedItem
340 }
341 Err(err) => ApplyAggregationResult::Error(err),
342 },
343
344 AggregationKind::Reaction { key, sender, timestamp } => {
345 let Some(reactions) = event.content().reactions() else {
346 return ApplyAggregationResult::LeftItemIntact;
348 };
349
350 let previous_reaction = reactions.get(key).and_then(|by_user| by_user.get(sender));
351
352 let is_same = previous_reaction.is_some_and(|prev| {
354 prev.timestamp == *timestamp
355 && same_send_state_kind(prev.send_state.as_ref(), self.send_state.as_ref())
356 });
357
358 if is_same {
359 ApplyAggregationResult::LeftItemIntact
360 } else {
361 let reactions = event
362 .to_mut()
363 .content_mut()
364 .reactions_mut()
365 .expect("reactions was Some above");
366
367 reactions.entry(key.clone()).or_default().insert(
368 sender.clone(),
369 ReactionInfo { timestamp: *timestamp, send_state: self.send_state.clone() },
370 );
371
372 ApplyAggregationResult::UpdatedItem
373 }
374 }
375
376 AggregationKind::Edit(_) => {
377 ApplyAggregationResult::Edit
379 }
380
381 AggregationKind::BeaconUpdate { location } => {
382 match live_location_state_from_item(event) {
383 Ok(state) => {
384 state.add_location(location.clone());
385 ApplyAggregationResult::UpdatedItem
386 }
387 Err(err) => ApplyAggregationResult::Error(err),
388 }
389 }
390
391 AggregationKind::BeaconStop { content } => match live_location_state_from_item(event) {
392 Ok(state) => {
393 state.stop(content.clone());
394 ApplyAggregationResult::UpdatedItem
395 }
396 Err(err) => ApplyAggregationResult::Error(err),
397 },
398
399 AggregationKind::CallDeclined { sender } => {
400 match rtc_notification_declinations_from_item(event) {
401 Ok(declinations) => {
402 if declinations.contains(sender) {
403 ApplyAggregationResult::LeftItemIntact
404 } else {
405 declinations.push(sender.clone());
406 ApplyAggregationResult::UpdatedItem
407 }
408 }
409 Err(err) => ApplyAggregationResult::Error(err),
410 }
411 }
412 }
413 }
414
415 fn unapply(&self, event: &mut Cow<'_, EventTimelineItem>) -> ApplyAggregationResult {
424 match &self.kind {
425 AggregationKind::PollResponse { sender, timestamp, .. } => {
426 let state = match poll_state_from_item(event) {
427 Ok(state) => state,
428 Err(err) => return ApplyAggregationResult::Error(err),
429 };
430 state.remove_response(sender, *timestamp);
431 ApplyAggregationResult::UpdatedItem
432 }
433
434 AggregationKind::PollEnd { .. } => {
435 ApplyAggregationResult::Error(AggregationError::CantUndoPollEnd)
437 }
438
439 AggregationKind::Redaction => {
440 if self.is_local() {
441 if event.unredacted_item.is_some() {
442 *event = Cow::Owned(event.unredact());
444 ApplyAggregationResult::UpdatedItem
445 } else {
446 ApplyAggregationResult::LeftItemIntact
448 }
449 } else {
450 ApplyAggregationResult::Error(AggregationError::CantUndoRedaction)
452 }
453 }
454
455 AggregationKind::Reaction { key, sender, .. } => {
456 let Some(reactions) = event.content().reactions() else {
457 return ApplyAggregationResult::LeftItemIntact;
459 };
460
461 let had_entry =
466 reactions.get(key).and_then(|by_user| by_user.get(sender)).is_some();
467
468 if had_entry {
469 let reactions = event
470 .to_mut()
471 .content_mut()
472 .reactions_mut()
473 .expect("reactions was some above");
474 let by_user = reactions.get_mut(key);
475 if let Some(by_user) = by_user {
476 by_user.swap_remove(sender);
477 if by_user.is_empty() {
479 reactions.swap_remove(key);
480 }
481 }
482 ApplyAggregationResult::UpdatedItem
483 } else {
484 ApplyAggregationResult::LeftItemIntact
485 }
486 }
487
488 AggregationKind::Edit(_) => {
489 ApplyAggregationResult::Edit
491 }
492
493 AggregationKind::BeaconUpdate { location } => {
494 match live_location_state_from_item(event) {
495 Ok(state) => {
496 state.remove_location(location.ts);
497 ApplyAggregationResult::UpdatedItem
498 }
499 Err(err) => ApplyAggregationResult::Error(err),
500 }
501 }
502
503 AggregationKind::BeaconStop { .. } => {
504 ApplyAggregationResult::Error(AggregationError::CantUndoBeaconStop)
506 }
507
508 AggregationKind::CallDeclined { .. } => {
509 ApplyAggregationResult::Error(AggregationError::CantUndoRtcDecline)
511 }
512 }
513 }
514
515 fn apply_send_state(
518 &self,
519 siblings: &[Aggregation],
520 event: &mut Cow<'_, EventTimelineItem>,
521 ) -> bool {
522 match &self.kind {
523 AggregationKind::Reaction { key, sender, .. } => {
524 let has_entry = event
525 .content()
526 .reactions()
527 .and_then(|reactions| reactions.get(key)?.get(sender))
528 .is_some();
529 if !has_entry {
530 return false;
531 }
532 let reactions =
533 event.to_mut().content_mut().reactions_mut().expect("reactions was Some above");
534 if let Some(info) =
535 reactions.get_mut(key).and_then(|by_user| by_user.get_mut(sender))
536 {
537 info.send_state = self.send_state.clone();
538 }
539 true
540 }
541
542 AggregationKind::Edit(_) => {
543 event.to_mut().edit_send_state = edit_send_state(siblings);
544 true
545 }
546
547 AggregationKind::Redaction => {
548 event.to_mut().redaction_send_state = self.send_state.clone();
549 true
550 }
551
552 AggregationKind::PollResponse { .. }
553 | AggregationKind::PollEnd { .. }
554 | AggregationKind::BeaconUpdate { .. }
555 | AggregationKind::BeaconStop { .. }
556 | AggregationKind::CallDeclined { .. } => false,
557 }
558 }
559}
560
561#[derive(Clone, Debug, Default)]
563pub(crate) struct Aggregations {
564 related_events: HashMap<TimelineEventItemId, Vec<Aggregation>>,
566
567 inverted_map: HashMap<TimelineEventItemId, TimelineEventItemId>,
569
570 pending_beacon_stops: HashMap<OwnedUserId, Aggregation>,
578}
579
580impl Aggregations {
581 pub fn clear(&mut self) {
583 self.related_events.clear();
584 self.inverted_map.clear();
585 self.pending_beacon_stops.clear();
586 }
587
588 pub fn add_pending_beacon_stop(&mut self, sender: OwnedUserId, aggregation: Aggregation) {
594 self.pending_beacon_stops.insert(sender, aggregation);
595 }
596
597 fn promote_pending_beacon_stop(
608 &mut self,
609 sender: &OwnedUserId,
610 target_event_id: OwnedEventId,
611 start_content: &BeaconInfoEventContent,
612 ) {
613 if !start_content.live {
614 return;
615 }
616
617 let Some(stop) = self.pending_beacon_stops.remove(sender) else { return };
618
619 let AggregationKind::BeaconStop { content: stop_content } = &stop.kind else {
620 warn!("pending beacon stop has unexpected aggregation kind");
621 return;
622 };
623
624 if !beacon_info_matches(start_content, stop_content) {
625 trace!("discarding stale pending beacon stop (content mismatch)");
626 return;
627 }
628
629 let target = TimelineEventItemId::EventId(target_event_id);
630 self.add(target, stop);
631 }
632
633 pub fn add(&mut self, related_to: TimelineEventItemId, aggregation: Aggregation) {
636 if matches!(aggregation.kind, AggregationKind::Redaction) {
639 for agg in self.related_events.remove(&related_to).unwrap_or_default() {
640 self.inverted_map.remove(&agg.own_id);
641 }
642 }
643
644 if let Some(previous_aggregations) = self.related_events.get(&related_to)
647 && previous_aggregations
648 .iter()
649 .any(|agg| matches!(agg.kind, AggregationKind::Redaction))
650 {
651 return;
652 }
653
654 self.inverted_map.insert(aggregation.own_id.clone(), related_to.clone());
655
656 let related_events = self.related_events.entry(related_to).or_default();
669 if let Some(pos) = related_events.iter().position(|agg| agg.own_id == aggregation.own_id) {
670 related_events.remove(pos);
671 }
672 related_events.push(aggregation);
673 }
674
675 pub fn try_remove_aggregation(
687 &mut self,
688 aggregation_id: &TimelineEventItemId,
689 items: &mut ObservableItemsTransaction<'_>,
690 ) -> Result<bool, AggregationError> {
691 let Some(found) = self.inverted_map.get(aggregation_id) else { return Ok(false) };
692
693 let aggregation = if let Some(aggregations) = self.related_events.get_mut(found) {
695 let removed = aggregations
696 .iter()
697 .position(|agg| agg.own_id == *aggregation_id)
698 .map(|idx| aggregations.remove(idx));
699
700 if aggregations.is_empty() {
703 self.related_events.remove(found);
704 }
705
706 removed
707 } else {
708 None
709 };
710
711 let Some(aggregation) = aggregation else {
712 warn!(
713 "incorrect internal state: {aggregation_id:?} was present in the inverted map, \
714 not in related-to map."
715 );
716 return Ok(false);
717 };
718
719 if let Some((item_pos, item)) = rfind_event_by_item_id(items, found) {
720 let mut cowed = Cow::Borrowed(&*item);
721 match aggregation.unapply(&mut cowed) {
722 ApplyAggregationResult::UpdatedItem => {
723 trace!("removed aggregation");
724 items.replace(
725 item_pos,
726 TimelineItem::new(cowed.into_owned(), item.internal_id.to_owned()),
727 );
728 }
729 ApplyAggregationResult::LeftItemIntact => {}
730 ApplyAggregationResult::Error(err) => {
731 warn!("error when unapplying aggregation: {err}");
732 }
733 ApplyAggregationResult::Edit => {
734 let resolved = self
736 .related_events
737 .get(found)
738 .is_some_and(|aggregations| resolve_edits(aggregations, items, &mut cowed));
739 if !resolved {
743 if cowed.edit_send_state.is_none() {
744 return Ok(true);
745 }
746 cowed.to_mut().edit_send_state = None;
747 }
748 items.replace(
749 item_pos,
750 TimelineItem::new(cowed.into_owned(), item.internal_id.to_owned()),
751 );
752 }
753 }
754 } else {
755 info!("missing related-to item ({found:?}) for aggregation {aggregation_id:?}");
756 }
757
758 Ok(true)
759 }
760
761 pub fn apply_all(
773 &mut self,
774 item_id: &TimelineEventItemId,
775 sender: &OwnedUserId,
776 event: &mut Cow<'_, EventTimelineItem>,
777 items: &mut ObservableItemsTransaction<'_>,
778 rules: &RoomVersionRules,
779 ) -> Result<(), AggregationError> {
780 if let TimelineEventItemId::EventId(event_id) = item_id
789 && let Some(live_location) = event.content().as_live_location_state()
790 {
791 self.promote_pending_beacon_stop(sender, event_id.clone(), &live_location.beacon_info);
792 }
793
794 let Some(aggregations) = self.related_events.get(item_id) else {
795 return Ok(());
796 };
797
798 let mut has_edits = false;
799
800 for a in aggregations {
801 match a.apply(event, rules) {
802 ApplyAggregationResult::Edit => {
803 has_edits = true;
804 }
805 ApplyAggregationResult::UpdatedItem | ApplyAggregationResult::LeftItemIntact => {}
806 ApplyAggregationResult::Error(err) => return Err(err),
807 }
808 }
809
810 if has_edits {
811 resolve_edits(aggregations, items, event);
812 }
813
814 Ok(())
815 }
816
817 pub fn mark_target_as_sent(&mut self, txn_id: OwnedTransactionId, event_id: OwnedEventId) {
821 let from = TimelineEventItemId::TransactionId(txn_id);
822 let to = TimelineEventItemId::EventId(event_id);
823
824 if let Some(aggregations) = self.related_events.remove(&from) {
826 for a in &aggregations {
828 if let Some(prev_target) = self.inverted_map.remove(&a.own_id) {
829 debug_assert_eq!(prev_target, from);
830 self.inverted_map.insert(a.own_id.clone(), to.clone());
831 }
832 }
833 self.related_events.entry(to).or_default().extend(aggregations);
835 }
836 }
837
838 pub fn update_send_state(
843 &mut self,
844 txn_id: OwnedTransactionId,
845 send_state: EventSendState,
846 items: &mut ObservableItemsTransaction<'_>,
847 rules: &RoomVersionRules,
848 ) -> bool {
849 let from = TimelineEventItemId::TransactionId(txn_id);
850
851 let Some(target) = self.inverted_map.get(&from).cloned() else {
852 return false;
853 };
854
855 let sent_event_id =
856 as_variant!(&send_state, EventSendState::Sent { event_id } => event_id.clone());
857
858 if let Some(event_id) = &sent_event_id {
859 let to = TimelineEventItemId::EventId(event_id.clone());
860 let remote_echo_received = self
861 .related_events
862 .get(&target)
863 .is_some_and(|aggs| aggs.iter().any(|agg| agg.own_id == to));
864 if remote_echo_received {
865 let remote = self.related_events.get_mut(&target).and_then(|aggs| {
868 aggs.retain(|agg| agg.own_id != from);
869 aggs.iter().find(|agg| agg.own_id == to).cloned()
870 });
871 self.inverted_map.remove(&from);
872 if let Some(remote) = remote {
873 find_item_and_apply_aggregation(self, items, &target, remote, rules);
874 }
875 return true;
876 }
877 }
878
879 let updated = {
880 let Some(aggregations) = self.related_events.get_mut(&target) else {
881 return false;
882 };
883 let Some(found) = aggregations.iter_mut().find(|agg| agg.own_id == from) else {
884 return false;
885 };
886
887 found.send_state = Some(send_state);
888
889 if let Some(event_id) = &sent_event_id {
890 found.own_id = TimelineEventItemId::EventId(event_id.clone());
891 }
892
893 found.clone()
894 };
895
896 if let Some(event_id) = sent_event_id {
897 self.inverted_map.remove(&from);
898 self.inverted_map.insert(TimelineEventItemId::EventId(event_id), target.clone());
899 }
900
901 let sent_redaction = matches!(updated.kind, AggregationKind::Redaction)
902 && matches!(updated.send_state, Some(EventSendState::Sent { .. }));
903
904 if sent_redaction {
905 find_item_and_apply_aggregation(self, items, &target, updated, rules);
907 } else if let Some((idx, item)) = rfind_event_by_item_id(items, &target) {
908 let siblings = self.related_events.get(&target).map(Vec::as_slice).unwrap_or(&[]);
909 let mut cowed = Cow::Borrowed(&*item);
910 if updated.apply_send_state(siblings, &mut cowed) {
911 let new_item = TimelineItem::new(cowed.into_owned(), item.internal_id.to_owned());
912 items.replace(idx, new_item);
913 }
914 } else {
915 trace!("couldn't find aggregation's target {target:?} to reflect its send state");
916 }
917
918 true
919 }
920
921 pub fn is_aggregation_of(&self, item: &TimelineEventItemId) -> Option<&TimelineEventItemId> {
924 self.inverted_map.get(item)
925 }
926
927 pub fn find_reaction(
930 &self,
931 target: &TimelineEventItemId,
932 key: &str,
933 sender: &UserId,
934 ) -> Option<&Aggregation> {
935 self.related_events.get(target)?.iter().rev().find(|agg| {
936 matches!(&agg.kind, AggregationKind::Reaction { key: k, sender: s, .. } if k == key && s == sender)
937 })
938 }
939}
940
941fn resolve_edits(
946 aggregations: &[Aggregation],
947 items: &ObservableItemsTransaction<'_>,
948 event: &mut Cow<'_, EventTimelineItem>,
949) -> bool {
950 let mut best_edit: Option<(PendingEdit, bool)> = None;
955 let mut best_edit_pos = None;
956
957 for a in aggregations {
958 if let AggregationKind::Edit(pending_edit) = &a.kind {
959 if a.send_state.is_some() {
962 best_edit = Some((pending_edit.clone(), true));
963 break;
964 }
965
966 match &a.own_id {
967 TimelineEventItemId::TransactionId(_) => {
968 best_edit = Some((pending_edit.clone(), true));
970 break;
971 }
972
973 TimelineEventItemId::EventId(event_id) => {
974 if let Some(best_edit_pos) = &mut best_edit_pos {
975 let pos = items.position_by_event_id(
978 pending_edit.bundled_item_owner.as_ref().unwrap_or(event_id),
979 );
980
981 if let Some(pos) = pos {
982 if pos > *best_edit_pos {
985 best_edit = Some((pending_edit.clone(), false));
986 *best_edit_pos = pos;
987 trace!(?best_edit_pos, edit_id = ?a.own_id, "found better edit");
988 }
989 } else {
990 trace!(edit_id = ?a.own_id, "couldn't find timeline meta for edit event");
991
992 if best_edit.is_none() {
996 best_edit = Some((pending_edit.clone(), false));
997 trace!(?best_edit_pos, edit_id = ?a.own_id, "found bundled edit");
998 }
999 }
1000 } else {
1001 best_edit = Some((pending_edit.clone(), false));
1004 best_edit_pos = items.position_by_event_id(event_id);
1005 trace!(?best_edit_pos, edit_id = ?a.own_id, "first best edit");
1006 }
1007 }
1008 }
1009 }
1010 }
1011
1012 if let Some((edit, is_local_echo)) = best_edit {
1013 if edit_item(event, edit, is_local_echo) {
1014 event.to_mut().edit_send_state = edit_send_state(aggregations);
1015 true
1016 } else {
1017 false
1018 }
1019 } else {
1020 false
1021 }
1022}
1023
1024fn edit_item(
1029 item: &mut Cow<'_, EventTimelineItem>,
1030 edit: PendingEdit,
1031 is_local_echo: bool,
1032) -> bool {
1033 if !is_local_echo {
1042 let Some(original_json) = item.original_json() else {
1043 error!("The original event does not have the JSON field set.");
1044 return false;
1045 };
1046
1047 let Some(edit_json) = &edit.edit_json else {
1048 error!(
1049 "The replacement event of a remotely received edit does not have the JSON field set."
1050 );
1051 return false;
1052 };
1053
1054 match check_validity_of_replacement_events(
1055 original_json,
1056 item.encryption_info(),
1057 edit_json,
1058 edit.encryption_info.as_deref(),
1059 ) {
1060 Ok(content) => content,
1061 Err(e) => {
1062 warn!("Event wasn't replaced due to the replacement event being invalid: {e}");
1063 return false;
1064 }
1065 }
1066 }
1067
1068 let TimelineItemContent::MsgLike(content) = item.content() else {
1069 info!("Edit of message event applies to {:?}, discarding", item.content().debug_string());
1070 return false;
1071 };
1072
1073 let PendingEdit { kind: edit_kind, edit_json, encryption_info, bundled_item_owner: _ } = edit;
1074
1075 match (edit_kind, content) {
1076 (
1077 PendingEditKind::RoomMessage(replacement),
1078 MsgLikeContent { kind: MsgLikeKind::Message(msg), .. },
1079 ) => {
1080 let mut new_msg = msg.clone();
1082 new_msg.apply_edit(replacement.new_content);
1083
1084 let new_item = item.with_content_and_latest_edit(
1085 TimelineItemContent::MsgLike(content.with_kind(MsgLikeKind::Message(new_msg))),
1086 edit_json,
1087 );
1088 *item = Cow::Owned(new_item);
1089 }
1090
1091 (
1092 PendingEditKind::Poll(replacement),
1093 MsgLikeContent { kind: MsgLikeKind::Poll(poll_state), .. },
1094 ) => {
1095 if let Some(new_poll_state) = poll_state.edit(replacement.new_content) {
1097 let new_item = item.with_content_and_latest_edit(
1098 TimelineItemContent::MsgLike(
1099 content.with_kind(MsgLikeKind::Poll(new_poll_state)),
1100 ),
1101 edit_json,
1102 );
1103 *item = Cow::Owned(new_item);
1104 } else {
1105 return false;
1107 }
1108 }
1109
1110 (edit_kind, _) => {
1111 info!(
1113 content = item.content().debug_string(),
1114 edit = format!("{:?}", edit_kind),
1115 "Mismatch between edit type and content type",
1116 );
1117 return false;
1118 }
1119 }
1120
1121 if let Some(encryption_info) = encryption_info {
1122 *item = Cow::Owned(item.with_encryption_info(Some(encryption_info)));
1123 }
1124
1125 true
1126}
1127
1128fn same_send_state_kind(a: Option<&EventSendState>, b: Option<&EventSendState>) -> bool {
1131 match (a, b) {
1132 (None, None) => true,
1133 (Some(a), Some(b)) => std::mem::discriminant(a) == std::mem::discriminant(b),
1134 _ => false,
1135 }
1136}
1137
1138fn edit_send_state(aggregations: &[Aggregation]) -> Option<EventSendState> {
1141 let rank = |s: &EventSendState| match s {
1142 EventSendState::SendingFailed { .. } => 2,
1143 EventSendState::NotSentYet { .. } => 1,
1144 EventSendState::Sent { .. } => 0,
1145 };
1146 aggregations
1147 .iter()
1148 .filter(|a| matches!(a.kind, AggregationKind::Edit(_)))
1149 .filter_map(|a| a.send_state.as_ref())
1150 .max_by_key(|s| rank(s))
1151 .cloned()
1152}
1153
1154pub(crate) fn find_item_and_apply_aggregation(
1160 aggregations: &Aggregations,
1161 items: &mut ObservableItemsTransaction<'_>,
1162 target: &TimelineEventItemId,
1163 aggregation: Aggregation,
1164 rules: &RoomVersionRules,
1165) -> Option<EventTimelineItem> {
1166 let Some((idx, event_item)) = rfind_event_by_item_id(items, target) else {
1167 trace!("couldn't find aggregation's target {target:?}");
1168 return None;
1169 };
1170
1171 let mut cowed = Cow::Borrowed(&*event_item);
1172 match aggregation.apply(&mut cowed, rules) {
1173 ApplyAggregationResult::UpdatedItem => {
1174 trace!("applied aggregation");
1175 let new_event_item = cowed.into_owned();
1176 let new_item =
1177 TimelineItem::new(new_event_item.clone(), event_item.internal_id.to_owned());
1178 items.replace(idx, new_item);
1179 Some(new_event_item)
1180 }
1181 ApplyAggregationResult::Edit => {
1182 if let Some(aggregations) = aggregations.related_events.get(target)
1183 && resolve_edits(aggregations, items, &mut cowed)
1184 {
1185 let new_event_item = cowed.into_owned();
1186 let new_item =
1187 TimelineItem::new(new_event_item.clone(), event_item.internal_id.to_owned());
1188 items.replace(idx, new_item);
1189 return Some(new_event_item);
1190 }
1191 None
1192 }
1193 ApplyAggregationResult::LeftItemIntact => {
1194 trace!("applying the aggregation had no effect");
1195 None
1196 }
1197 ApplyAggregationResult::Error(err) => {
1198 warn!("error when applying aggregation: {err}");
1199 None
1200 }
1201 }
1202}
1203
1204enum ApplyAggregationResult {
1206 UpdatedItem,
1208
1209 Edit,
1212
1213 LeftItemIntact,
1216
1217 Error(AggregationError),
1219}
1220
1221#[derive(Debug, thiserror::Error)]
1222pub(crate) enum AggregationError {
1223 #[error("trying to end a poll twice")]
1224 PollAlreadyEnded,
1225
1226 #[error("a poll end can't be unapplied")]
1227 CantUndoPollEnd,
1228
1229 #[error("a redaction can't be unapplied")]
1230 CantUndoRedaction,
1231
1232 #[error("a beacon stop can't be unapplied")]
1233 CantUndoBeaconStop,
1234
1235 #[error("a call decline can't be unapplied")]
1236 CantUndoRtcDecline,
1237
1238 #[error(
1239 "trying to apply an aggregation of one type to an invalid target: \
1240 expected {expected}, actual {actual}"
1241 )]
1242 InvalidType { expected: String, actual: String },
1243}