Skip to content

Commit 4fb6989

Browse files
committed
doc(timeline): add doc and changelog for the new aggregation system
1 parent 3578770 commit 4fb6989

File tree

2 files changed

+114
-11
lines changed

2 files changed

+114
-11
lines changed

crates/matrix-sdk-ui/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ All notable changes to this project will be documented in this file.
66

77
## [Unreleased] - ReleaseDate
88

9+
### Bug Fixes
10+
11+
### Features
12+
13+
### Refactor
14+
15+
- [**breaking**] Reactions on a given timeline item have been moved from
16+
[`EventTimelineItem::reactions()`] to [`TimelineItemContent::reactions()`]; they're thus available
17+
from an [`EventTimelineItem`] by calling `.content().reactions()`. They're also returned by
18+
ownership (cloned) instead of by reference.
19+
([#4576](https://github.com/matrix-org/matrix-rust-sdk/pull/4576))
20+
921
## [0.10.0] - 2025-02-04
1022

1123
### Bug Fixes

crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,31 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
//! An aggregation manager for the timeline.
16+
//!
17+
//! An aggregation is an event that relates to another event: for instance, a
18+
//! reaction, a poll response, and so on and so forth.
19+
//!
20+
//! Because of the sync mechanisms and federation, it can happen that a related
21+
//! event is received *before* receiving the event it relates to. Those events
22+
//! must be accounted for, stashed somewhere, and reapplied later, if/when the
23+
//! related-to event shows up.
24+
//!
25+
//! In addition to that, a room's event cache can also decide to move events
26+
//! around, in its own internal representation (likely because it ran into some
27+
//! duplicate events). When that happens, a timeline opened on the given room
28+
//! will see a removal then re-insertion of the given event. If that event was
29+
//! the target of aggregations, then those aggregations must be re-applied when
30+
//! the given event is reinserted.
31+
//!
32+
//! To satisfy both requirements, the [`Aggregations`] "manager" object provided
33+
//! by this module will take care of memoizing aggregations, for the entire
34+
//! lifetime of the timeline (or until it's [`Aggregations::clear()`]'ed by some
35+
//! caller). Aggregations are saved in memory, and have the same lifetime as
36+
//! that of a timeline. This makes it possible to apply pending aggregations
37+
//! to cater for the first use case, and to never lose any aggregations in the
38+
//! second use case.
39+
1540
use std::collections::HashMap;
1641

1742
use ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId};
@@ -21,32 +46,60 @@ use crate::timeline::{
2146
PollState, ReactionInfo, ReactionStatus, TimelineEventItemId, TimelineItemContent,
2247
};
2348

49+
/// Which kind of aggregation (related event) is this?
2450
#[derive(Clone, Debug)]
2551
pub(crate) enum AggregationKind {
52+
/// This is a response to a poll.
2653
PollResponse {
54+
/// Sender of the poll's response.
2755
sender: OwnedUserId,
56+
/// Timestamp at which the response has beens ent.
2857
timestamp: MilliSecondsSinceUnixEpoch,
58+
/// All the answers to the poll sent by the sender.
2959
answers: Vec<String>,
3060
},
3161

62+
/// This is the marker of the end of a poll.
3263
PollEnd {
64+
/// Timestamp at which the poll ends, i.e. all the responses with a
65+
/// timestamp prior to this one should be taken into account
66+
/// (and all the responses with a timestamp after this one
67+
/// should be dropped).
3368
end_date: MilliSecondsSinceUnixEpoch,
3469
},
3570

71+
/// This is a reaction to another event.
3672
Reaction {
73+
/// The reaction "key" displayed by the client, often an emoji.
3774
key: String,
75+
/// Sender of the reaction.
3876
sender: OwnedUserId,
77+
/// Timestamp at which the reaction has been sent.
3978
timestamp: MilliSecondsSinceUnixEpoch,
79+
/// The send status of the reaction this is, with handles to abort it if
80+
/// we can, etc.
4081
reaction_status: ReactionStatus,
4182
},
4283
}
4384

85+
/// An aggregation is an event related to another event (for instance a
86+
/// reaction, a poll's response, etc.).
87+
///
88+
/// It can be either a local or a remote echo.
4489
#[derive(Clone, Debug)]
4590
pub(crate) struct Aggregation {
91+
/// The kind of aggregation this represents.
4692
pub kind: AggregationKind,
93+
94+
/// The own timeline identifier for a reaction.
95+
///
96+
/// It will be a transaction id when the aggregation is still a local echo,
97+
/// and it will transition into an event id when the aggregation is a
98+
/// remote echo (i.e. has been received in a sync response):
4799
pub own_id: TimelineEventItemId,
48100
}
49101

102+
/// Get the poll state from a given [`TimelineItemContent`].
50103
fn poll_state_from_item(
51104
content: &mut TimelineItemContent,
52105
) -> Result<&mut PollState, AggregationError> {
@@ -60,15 +113,20 @@ fn poll_state_from_item(
60113
}
61114

62115
impl Aggregation {
116+
/// Create a new [`Aggregation`].
63117
pub fn new(own_id: TimelineEventItemId, kind: AggregationKind) -> Self {
64118
Self { kind, own_id }
65119
}
66120

67121
/// Apply an aggregation in-place to a given [`TimelineItemContent`].
68122
///
69123
/// In case of success, returns a boolean indicating whether applying the
70-
/// aggregation caused a change in the content. In case of error,
71-
/// returns an error detailing why the aggregation couldn't be applied.
124+
/// aggregation caused a change in the content. If the aggregation could be
125+
/// applied, but it didn't cause any change in the passed
126+
/// [`TimelineItemContent`], `Ok(false)` will be returned.
127+
///
128+
/// In case of error, returns an error detailing why the aggregation
129+
/// couldn't be applied.
72130
pub fn apply(&self, content: &mut TimelineItemContent) -> Result<bool, AggregationError> {
73131
match &self.kind {
74132
AggregationKind::PollResponse { sender, timestamp, answers } => {
@@ -122,9 +180,13 @@ impl Aggregation {
122180

123181
/// Undo an aggregation in-place to a given [`TimelineItemContent`].
124182
///
125-
/// In case of success, returns a boolean indicating whether applying the
126-
/// aggregation caused a change in the content. In case of error,
127-
/// returns an error detailing why the aggregation couldn't be applied.
183+
/// In case of success, returns a boolean indicating whether unapplying the
184+
/// aggregation caused a change in the content. If the aggregation could be
185+
/// unapplied, but it didn't cause any change in the passed
186+
/// [`TimelineItemContent`], `Ok(false)` will be returned.
187+
///
188+
/// In case of error, returns an error detailing why the aggregation
189+
/// couldn't be applied.
128190
pub fn unapply(&self, content: &mut TimelineItemContent) -> Result<bool, AggregationError> {
129191
match &self.kind {
130192
AggregationKind::PollResponse { sender, timestamp, .. } => {
@@ -172,11 +234,14 @@ pub(crate) struct Aggregations {
172234
}
173235

174236
impl Aggregations {
237+
/// Clear all the known aggregations from all the mappings.
175238
pub fn clear(&mut self) {
176239
self.related_events.clear();
177240
self.inverted_map.clear();
178241
}
179242

243+
/// Add a given aggregation that relates to the [`TimelineItemContent`]
244+
/// identified by the given [`TimelineEventItemId`].
180245
pub fn add(&mut self, related_to: TimelineEventItemId, aggregation: Aggregation) {
181246
self.inverted_map.insert(aggregation.own_id.clone(), related_to.clone());
182247
self.related_events.entry(related_to).or_default().push(aggregation);
@@ -192,20 +257,35 @@ impl Aggregations {
192257
let found = self.inverted_map.get(aggregation_id)?;
193258

194259
// Find and remove the aggregation in the other mapping.
195-
let aggregation = self.related_events.get_mut(found).and_then(|aggregations| {
196-
aggregations
260+
let aggregation = if let Some(aggregations) = self.related_events.get_mut(found) {
261+
let removed = aggregations
197262
.iter()
198263
.position(|agg| agg.own_id == *aggregation_id)
199-
.map(|idx| aggregations.remove(idx))
200-
});
264+
.map(|idx| aggregations.remove(idx));
265+
266+
// If this was the last aggregation, remove the entry in the `related_events`
267+
// mapping.
268+
if aggregations.is_empty() {
269+
self.related_events.remove(found);
270+
}
271+
272+
removed
273+
} else {
274+
None
275+
};
201276

202277
if aggregation.is_none() {
203-
warn!("unexpected missing aggregation {aggregation_id:?} (was present in the inverted map, not in the actual map)");
278+
warn!("incorrect internal state: {aggregation_id:?} was present in the inverted map, not in related-to map.");
204279
}
205280

206281
Some((found, aggregation?))
207282
}
208283

284+
/// Apply all the aggregations to a [`TimelineItemContent`].
285+
///
286+
/// Will return an error at the first aggregation that couldn't be applied;
287+
/// see [`Aggregation::apply`] which explains under which conditions it can
288+
/// happen.
209289
pub fn apply(
210290
&self,
211291
item_id: &TimelineEventItemId,
@@ -237,13 +317,18 @@ impl Aggregations {
237317
}
238318
}
239319
// Update the direct mapping of target -> aggregations.
240-
self.related_events.insert(to, aggregations);
320+
self.related_events.entry(to).or_default().extend(aggregations);
241321
}
242322
}
243323

244324
/// Mark an aggregation event as being sent (i.e. it transitions from an
245325
/// local transaction id to its remote event id counterpart), by
246326
/// updating the internal mappings.
327+
///
328+
/// When an aggregation has been marked as sent, it may need to be reapplied
329+
/// to the corresponding [`TimelineItemContent`]; in this case, a
330+
/// [`MarkAggregationSentResult::MarkedSent`] result with a set `update`
331+
/// will be returned, and must be applied.
247332
pub fn mark_aggregation_as_sent(
248333
&mut self,
249334
txn_id: OwnedTransactionId,
@@ -284,8 +369,14 @@ impl Aggregations {
284369
}
285370
}
286371

372+
/// The result of marking an aggregation as sent.
287373
pub(crate) enum MarkAggregationSentResult {
374+
/// The aggregation has been found, and marked as sent.
375+
///
376+
/// Optionally, it can include an [`Aggregation`] `update` to the matching
377+
/// [`TimelineItemContent`] item identified by the [`TimelineEventItemId`].
288378
MarkedSent { update: Option<(TimelineEventItemId, Aggregation)> },
379+
/// The aggregation was unknown to the aggregations manager, aka not found.
289380
NotFound,
290381
}
291382

0 commit comments

Comments
 (0)