Commit 960ac0721079
tarball14 files changed+710-314
@@ -75,6 +75,20 @@ out, so no stale diff can land after it
7575
7676
`TimelineId` tells a room from a thread by its root event7777
78
+every item is an event, a day divider, the read marker or the start. an event79
+carries its sender with avatar and name ambiguity, and a `TimelineContent`80
+saying what it actually is. messages keep their msgtype, membership and profile81
+changes and the common room state changes get their own variants, and anything82
+else is `OtherState` or `Other` with its type. nothing is flattened into english83
+in a body field84
+85
+replies carry the replied-to event once it's loaded. the watcher asks the sdk86
+for any it hasn't got, once per event, since the sdk won't on its own87
+88
+edit, redact and react take an `EventRef`, so your own message can be edited or89
+reacted to before the server has echoed it back. redacting an unsent one just90
+cancels it91
+7892
## mutations7993
8094
membership, moderation, power levels, room creation and account data writes go@@ -116,6 +130,21 @@ replace it atomically with `ClientHandle::session_record()` on every
116130
store. a soft logout keeps the store for a same-device login, or the e2ee keys117131
go with it. a bare `Stopped` kills nothing. one store directory per login118132
133
+## room lists, spaces and profiles134
+135
+`RoomSummary` has what a room list row needs without a second query. topic,136
+alias, member counts, heroes for nameless rooms, the inviter, where an upgraded137
+room went, a latest event preview, the joined spaces listing it, and for a138
+space the children it lists139
+140
+parents come from one lazily built `SpaceService` shared by every handle.141
+children come straight from the space's local state, joined or not.142
+`space_hierarchy` pages through the server's view for browsing rooms you143
+haven't joined, and holds its place between pages144
+145
+`own_profile` and `user_profile` read profiles. name and avatar changes are146
+commands, and uploading an avatar gets the same byte cap as any upload147
+119148
## notifications and room data120149
121150
the notification client is built lazily and shares the engine's sync service.@@ -210,7 +210,7 @@ fetch changes no state, yet it is a `MediaCommand` whose bytes arrive as a
210210
other frontend's image bytes, up to 20 MiB each, and the requester has to211211
filter by `MediaId`.212212
213
-### A3. Every timeline item is a `Message`, with presentation strings in `body`. open, q3213
+### A3. Every timeline item is a `Message`, with presentation strings in `body`. done214214
215215
`normalise_timeline_item` (`src/normalise/timeline.rs`) flattens every event216216
into `TimelineMessage`. The consequences for a client are these.@@ -235,7 +235,7 @@ This needs a content enum, for example `Message { msgtype, ... }`, `Redacted`,
235235
`UnableToDecrypt`, `Membership`, `ProfileChange`, `RoomState`, `Poll` and236236
`Other { event_type }`. It is the largest single rendering gap.237237
238
-### A4. Timeline commands cannot target local echoes. open, lands with q3238
+### A4. Timeline commands cannot target local echoes. done239239
240240
`Edit`, `Redact` and `ToggleReaction` take an `OwnedEventId` and wrap it in241241
`TimelineEventItemId::EventId`. The SDK accepts transaction ids for all three,@@ -243,7 +243,7 @@ but a client cannot edit, redact or react to a message it sent until the
243243
server echo arrives. The commands should take the identifier that the model244244
already exposes.245245
246
-### A5. `RoomSummary` is too thin for a room list. open, lands with the malice queries and q2246
+### A5. `RoomSummary` is too thin for a room list. done247247
248248
Missing fields, all cheap SDK reads. Topic, canonical alias, joined and invited249249
member counts, heroes, tombstone and successor, inviter for invites, a latest@@ -1,6 +1,6 @@
11
use macltok::{2
- EngineConfig, Event, PasswordLogin, RoomMembership, Session, StoreConfig, TimelineId,3
- TimelineItem,2
+ EngineConfig, Event, PasswordLogin, RoomMembership, Session, StoreConfig, TimelineContent,3
+ TimelineId, TimelineItem,44
};55
66
#[tokio::main]@@ -82,8 +82,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
8282
8383
fn print_messages(items: &[TimelineItem]) {8484
for item in items {85
- if let TimelineItem::Message(message) = item {86
- println!("{} {}", message.sender_name, message.body);85
+ if let TimelineItem::Event(event) = item86
+ && let TimelineContent::Message(message) = &event.content87
+ {88
+ println!("{} {}", event.sender.name(), message.body);8789
}8890
}8991
}@@ -1,4 +1,4 @@
1
-use std::{future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc};1
+use std::{collections::HashSet, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc};22
33
use futures_util::{FutureExt, StreamExt};44
use matrix_sdk::{@@ -22,8 +22,8 @@ use tokio::{
2222
};2323
2424
use crate::{25
- Error, ErrorKind, Event, HistoryState, MessageContent, MessageEditContent, MessageMentions,26
- RichText, TimelineCommand, TimelineId,25
+ Error, ErrorKind, Event, EventRef, HistoryState, MessageContent, MessageEditContent,26
+ MessageMentions, RichText, TimelineCommand, TimelineId,2727
engine::MediaService,2828
normalise::{MediaScope, normalise_timeline, normalise_timeline_diffs},2929
transport::{EventSender, Responder, SendEventError},@@ -163,17 +163,15 @@ impl TimelineSession {
163163
let background: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> = match command {164164
TimelineCommand::Paginate { limit, .. } => return self.paginate(limit, Some(reply)),165165
// network-bound, so it runs beside the actor instead of queueing the room behind it166
- TimelineCommand::Redact {167
- event_id, reason, ..168
- } => Box::pin(async move {166
+ TimelineCommand::Redact { target, reason, .. } => Box::pin(async move {169167
timeline170
- .redact(&TimelineEventItemId::EventId(event_id), reason.as_deref())168
+ .redact(&item_id(target), reason.as_deref())171169
.await172170
.map_err(timeline_error)173171
}),174
- TimelineCommand::ToggleReaction { event_id, key, .. } => Box::pin(async move {172
+ TimelineCommand::ToggleReaction { target, key, .. } => Box::pin(async move {175173
timeline176
- .toggle_reaction(&TimelineEventItemId::EventId(event_id), &key)174
+ .toggle_reaction(&item_id(target), &key)177175
.await178176
.map(drop)179177
.map_err(timeline_error)@@ -235,7 +233,7 @@ impl TimelineSession {
235233
.map_err(timeline_error)236234
}237235
TimelineCommand::Edit {238
- event_id, content, ..236
+ target, content, ..239237
} => {240238
let (content, mentions, emote) = match content {241239
MessageEditContent::Text { content, mentions } => (content, mentions, false),@@ -246,7 +244,7 @@ impl TimelineSession {
246244
}247245
timeline248246
.edit(249
- &TimelineEventItemId::EventId(event_id),247
+ &item_id(target),250248
EditedContent::RoomMessage(message_content(content, mentions, emote)),251249
)252250
.await@@ -312,6 +310,35 @@ impl Drop for TimelineSession {
312310
}313311
}314312
313
+fn item_id(target: EventRef) -> TimelineEventItemId {314
+ match target {315
+ EventRef::Remote(event_id) => TimelineEventItemId::EventId(event_id),316
+ EventRef::Local(transaction_id) => TimelineEventItemId::TransactionId(transaction_id),317
+ }318
+}319
+320
+/// the sdk leaves a reply's target unloaded until someone asks, so the watcher asks, once per event321
+struct ReplyFetcher {322
+ timeline: Arc<Timeline>,323
+ asked: HashSet<OwnedEventId>,324
+ fetches: JoinSet<()>,325
+}326
+327
+impl ReplyFetcher {328
+ fn fetch(&mut self, missing: Vec<OwnedEventId>) {329
+ for event_id in missing {330
+ if self.asked.insert(event_id.clone()) {331
+ let timeline = self.timeline.clone();332
+ self.fetches.spawn(async move {333
+ if let Err(error) = timeline.fetch_details_for_event(&event_id).await {334
+ tracing::debug!(%event_id, %error, "fetching reply details failed");335
+ }336
+ });337
+ }338
+ }339
+ }340
+}341
+315342
async fn start_watcher(316343
id: &TimelineId,317344
timeline: &Arc<Timeline>,@@ -329,14 +356,28 @@ async fn start_watcher(
329356
items: normalised.items,330357
})331358
.await?;359
+ let mut replies = ReplyFetcher {360
+ timeline: timeline.clone(),361
+ asked: HashSet::new(),362
+ fetches: JoinSet::new(),363
+ };364
+ replies.fetch(normalised.missing_replies);332365
let id = id.clone();333366
let events = context.events.clone();334367
let media = context.media.clone();335368
let scope = scope.clone();336369
Ok(context.supervisor.spawn("timeline watcher", async move {337
- while let Some(diffs) = stream.next().await {370
+ loop {371
+ let diffs = tokio::select! {372
+ diffs = stream.next() => match diffs {373
+ Some(diffs) => diffs,374
+ None => return,375
+ },376
+ Some(_) = replies.fetches.join_next(), if !replies.fetches.is_empty() => continue,377
+ };338378
let normalised = normalise_timeline_diffs(diffs, current_user_id.as_deref(), &scope);339379
media.register(normalised.media).await;380
+ replies.fetch(normalised.missing_replies);340381
let event = Event::TimelineChanged {341382
timeline_id: id.clone(),342383
diffs: normalised.diffs,@@ -21,18 +21,20 @@ pub use account::{
2121
pub use engine::{Engine, EngineConfig, EngineError};2222
pub use error::{Error, ErrorKind};2323
pub use model::{24
- AccountCommand, AttachmentUpload, Command, CommandOutput, ConnectionState, CreateRoomOptions,25
- EncryptionCommand, EncryptionState, EngineErrorKind, EngineFailure, Event, HistoryState,26
- KeyBackupState, LatestEvent, LifecycleCommand, Media, MediaCommand, MediaDescriptor, MediaId,27
- MediaKind, MediaSize, MemberFilter, MemberMembership, MemberSummary, MessageContent,28
- MessageEditContent, MessageMentions, NotificationCommand, OwnDevice, PowerLevelChanges,29
- Profile, PushNotification, PushNotificationEvent, PushNotificationRoom, PushNotificationSender,30
- PushNotificationStatus, Reaction, RecoveryState, ReplyReference, RichText, RoomCategory,31
- RoomCommand, RoomHero, RoomListFilter, RoomMatcher, RoomMembership, RoomNotificationMode,24
+ AccountCommand, AttachmentUpload, Change, Command, CommandOutput, ConnectionState,25
+ CreateRoomOptions, EncryptionCommand, EncryptionState, EngineErrorKind, EngineFailure, Event,26
+ EventRef, HistoryState, KeyBackupState, LatestEvent, LifecycleCommand, Media, MediaCommand,27
+ MediaDescriptor, MediaId, MediaKind, MediaSize, MemberFilter, MemberMembership, MemberSummary,28
+ MembershipChange, Message, MessageContent, MessageEditContent, MessageKind, MessageMentions,29
+ NotificationCommand, OwnDevice, PowerLevelChanges, Profile, PushNotification,30
+ PushNotificationEvent, PushNotificationRoom, PushNotificationSender, PushNotificationStatus,31
+ Reaction, RecoveryState, ReplyDetails, ReplyReference, RichText, RoomCategory, RoomCommand,32
+ RoomHero, RoomListFilter, RoomMatcher, RoomMembership, RoomNotificationMode,3233
RoomNotificationSettings, RoomPowerLevels, RoomPreset, RoomSummary, RoomVisibility, SendState,33
- SpaceChild, ThreadSummary, TimelineCommand, TimelineDiff, TimelineDiffError, TimelineId,34
- TimelineItem, TimelineItemId, TimelineMessage, TypingState, UploadDetails, UploadThumbnail,35
- VerificationAction, VerificationFailure, VerificationState, VerificationTarget,34
+ Sender, SpaceChild, ThreadSummary, TimelineCommand, TimelineContent, TimelineDiff,35
+ TimelineDiffError, TimelineEvent, TimelineId, TimelineItem, TimelineItemId, TypingState,36
+ UploadDetails, UploadThumbnail, UtdCause, VerificationAction, VerificationFailure,37
+ VerificationState, VerificationTarget,3638
};3739
pub use transport::{ClientHandle, EventStream, Pending, SpaceHierarchy};3840
pub use uiaa::{@@ -219,17 +219,18 @@ pub enum TimelineCommand {
219219
},220220
Edit {221221
timeline_id: TimelineId,222
- event_id: OwnedEventId,222
+ target: EventRef,223223
content: MessageEditContent,224224
},225
+ /// redacting your own unsent message just cancels it225226
Redact {226227
timeline_id: TimelineId,227
- event_id: OwnedEventId,228
+ target: EventRef,228229
reason: Option<String>,229230
},230231
ToggleReaction {231232
timeline_id: TimelineId,232
- event_id: OwnedEventId,233
+ target: EventRef,233234
key: String,234235
},235236
/// works on a closed room too, it just can't refuse to move the marker backwards@@ -1041,7 +1042,7 @@ pub struct OwnDevice {
10411042
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]10421043
#[non_exhaustive]10431044
pub enum TimelineItem {1044
- Message(Box<TimelineMessage>),1045
+ Event(Box<TimelineEvent>),10451046
DayDivider { timestamp_ms: u64 },10461047
ReadMarker,10471048
TimelineStart,@@ -1220,26 +1221,183 @@ impl TimelineItemId {
12201221
#[derive(Clone, Debug, Eq, PartialEq)]12211222
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]12221223
#[non_exhaustive]1223
-pub struct TimelineMessage {1224
+pub struct TimelineEvent {12241225
pub item_id: TimelineItemId,12251226
pub event_id: Option<OwnedEventId>,12261227
pub transaction_id: Option<OwnedTransactionId>,1227
- pub sender_id: OwnedUserId,1228
- pub sender_name: String,1229
- pub body: String,1230
- pub formatted_body: Option<String>,1231
- pub mentions: Option<MessageMentions>,1232
- pub thread_root: Option<OwnedEventId>,1233
- pub thread_summary: Option<ThreadSummary>,1228
+ pub sender: Sender,12341229
pub timestamp_ms: u64,12351230
pub own: bool,12361231
pub send_state: SendState,1237
- pub edited: bool,1238
- pub unable_to_decrypt: bool,1239
- pub media: Option<MediaDescriptor>,1232
+ pub content: TimelineContent,12401233
pub reply_to: Option<ReplyReference>,1234
+ pub thread_root: Option<OwnedEventId>,1235
+ pub thread_summary: Option<ThreadSummary>,12411236
pub reactions: Vec<Reaction>,12421237
pub read_by: Vec<OwnedUserId>,1238
+ pub editable: bool,1239
+}1240
+1241
+impl TimelineEvent {1242
+ /// what to hand edit, redact and react. works before the server has echoed it back1243
+ #[must_use]1244
+ pub fn target(&self) -> Option<EventRef> {1245
+ self.event_id1246
+ .clone()1247
+ .map(EventRef::Remote)1248
+ .or_else(|| self.transaction_id.clone().map(EventRef::Local))1249
+ }1250
+}1251
+1252
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]1253
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1254
+#[non_exhaustive]1255
+pub enum EventRef {1256
+ Remote(OwnedEventId),1257
+ /// a local echo, by the transaction id it was queued under1258
+ Local(OwnedTransactionId),1259
+}1260
+1261
+impl From<OwnedEventId> for EventRef {1262
+ fn from(event_id: OwnedEventId) -> Self {1263
+ Self::Remote(event_id)1264
+ }1265
+}1266
+1267
+#[derive(Clone, Debug, Eq, PartialEq)]1268
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1269
+#[non_exhaustive]1270
+pub struct Sender {1271
+ pub user_id: OwnedUserId,1272
+ pub display_name: Option<String>,1273
+ pub avatar: Option<OwnedMxcUri>,1274
+ /// someone else in the room goes by the same name, so show the user id too1275
+ pub name_ambiguous: bool,1276
+}1277
+1278
+impl Sender {1279
+ #[must_use]1280
+ pub fn name(&self) -> &str {1281
+ self.display_name1282
+ .as_deref()1283
+ .unwrap_or_else(|| self.user_id.as_str())1284
+ }1285
+}1286
+1287
+#[derive(Clone, Debug, Eq, PartialEq)]1288
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1289
+#[non_exhaustive]1290
+pub enum TimelineContent {1291
+ Message(Message),1292
+ Sticker {1293
+ body: String,1294
+ media: MediaDescriptor,1295
+ },1296
+ Poll {1297
+ question: String,1298
+ },1299
+ Redacted,1300
+ UnableToDecrypt(UtdCause),1301
+ Membership {1302
+ user_id: OwnedUserId,1303
+ display_name: Option<String>,1304
+ change: MembershipChange,1305
+ },1306
+ Profile {1307
+ user_id: OwnedUserId,1308
+ display_name: Option<Change<Option<String>>>,1309
+ avatar: Option<Change<Option<OwnedMxcUri>>>,1310
+ },1311
+ /// `None` means it was cleared1312
+ RoomName(Option<String>),1313
+ RoomTopic(Option<String>),1314
+ RoomAvatar(Option<OwnedMxcUri>),1315
+ EncryptionEnabled,1316
+ RoomCreated,1317
+ RoomUpgraded {1318
+ successor: OwnedRoomId,1319
+ },1320
+ OtherState {1321
+ event_type: String,1322
+ state_key: String,1323
+ },1324
+ Other {1325
+ event_type: String,1326
+ },1327
+}1328
+1329
+#[derive(Clone, Debug, Eq, PartialEq)]1330
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1331
+#[non_exhaustive]1332
+pub struct Message {1333
+ pub kind: MessageKind,1334
+ pub body: String,1335
+ pub formatted_body: Option<String>,1336
+ pub mentions: Option<MessageMentions>,1337
+ pub edited: bool,1338
+ pub media: Option<MediaDescriptor>,1339
+}1340
+1341
+#[derive(Clone, Debug, Eq, PartialEq)]1342
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1343
+#[non_exhaustive]1344
+pub enum MessageKind {1345
+ Text,1346
+ Emote,1347
+ Notice,1348
+ ServerNotice,1349
+ Image,1350
+ Video,1351
+ Audio,1352
+ File,1353
+ Location,1354
+ VerificationRequest,1355
+ Other(String),1356
+}1357
+1358
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]1359
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1360
+#[non_exhaustive]1361
+pub enum MembershipChange {1362
+ Joined,1363
+ Left,1364
+ Banned,1365
+ Unbanned,1366
+ Kicked,1367
+ KickedAndBanned,1368
+ Invited,1369
+ InvitationAccepted,1370
+ InvitationRejected,1371
+ InvitationRevoked,1372
+ Knocked,1373
+ KnockAccepted,1374
+ KnockRetracted,1375
+ KnockDenied,1376
+ /// nothing a person would call a change, or something the sdk couldn't make sense of1377
+ Other,1378
+}1379
+1380
+#[derive(Clone, Debug, Eq, PartialEq)]1381
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1382
+pub struct Change<T> {1383
+ pub old: T,1384
+ pub new: T,1385
+}1386
+1387
+/// why a message couldn't be read, worth telling the user since some of these they can fix1388
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]1389
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1390
+#[non_exhaustive]1391
+pub enum UtdCause {1392
+ Unknown,1393
+ SentBeforeWeJoined,1394
+ VerificationViolation,1395
+ UnsignedDevice,1396
+ UnknownDevice,1397
+ HistoricalMessageAndBackupIsDisabled,1398
+ WithheldForUnverifiedOrInsecureDevice,1399
+ WithheldBySender,1400
+ HistoricalMessageAndDeviceIsUnverified,12431401
}12441402
12451403
#[derive(Clone, Debug, Eq, PartialEq)]@@ -1266,8 +1424,21 @@ pub enum SendState {
12661424
#[non_exhaustive]12671425
pub struct ReplyReference {12681426
pub event_id: OwnedEventId,1269
- pub sender_id: Option<OwnedUserId>,1270
- pub sender_name: Option<String>,1427
+ pub details: ReplyDetails,1428
+}1429
+1430
+/// the replied-to event. macltok asks for it when it isn't loaded, `Pending` then turns into one of the others1431
+#[derive(Clone, Debug, Eq, PartialEq)]1432
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1433
+#[non_exhaustive]1434
+pub enum ReplyDetails {1435
+ Pending,1436
+ Ready {1437
+ sender: Sender,1438
+ content: Box<TimelineContent>,1439
+ timestamp_ms: u64,1440
+ },1441
+ Unavailable,12711442
}12721443
12731444
#[derive(Clone, Debug, Eq, PartialEq)]@@ -1,21 +1,27 @@
11
use std::sync::Arc;22
3
+use matrix_sdk_crypto::types::events::UtdCause as SdkUtdCause;34
use matrix_sdk_ui::{45
eyeball_im::VectorDiff,56
timeline::{6
- EventSendState, MsgLikeKind, TimelineDetails, TimelineEventItemId,7
- TimelineItem as SdkTimelineItem, TimelineItemContent, TimelineItemKind,8
- VirtualTimelineItem,7
+ AnyOtherStateEventContentChange, EncryptedMessage, EventSendState, EventTimelineItem,8
+ MembershipChange as SdkMembershipChange, MsgLikeContent, MsgLikeKind, Profile,9
+ TimelineDetails, TimelineEventItemId, TimelineItem as SdkTimelineItem, TimelineItemContent,10
+ TimelineItemKind, VirtualTimelineItem,911
},1012
};1113
use ruma::{12
- UserId,13
- events::room::message::{MessageFormat, MessageType},14
+ OwnedEventId, UserId,15
+ events::{16
+ StateEventContentChange,17
+ room::message::{MessageFormat, MessageType},18
+ },1419
};1520
1621
use crate::{17
- MessageMentions, Reaction, ReplyReference, SendState, ThreadSummary, TimelineDiff,18
- TimelineItem, TimelineItemId, TimelineMessage,22
+ Change, MembershipChange, Message, MessageKind, MessageMentions, Reaction, ReplyDetails,23
+ ReplyReference, SendState, Sender, ThreadSummary, TimelineContent, TimelineDiff, TimelineEvent,24
+ TimelineItem, TimelineItemId, UtdCause,1925
};2026
2127
use super::media::{@@ -23,9 +29,98 @@ use super::media::{
2329
media_identity, media_registration_for_item,2430
};2531
32
+#[derive(Default)]2633
pub(crate) struct NormalisedTimeline {2734
pub(crate) items: Vec<TimelineItem>,2835
pub(crate) media: Vec<MediaRegistration>,36
+ /// replies whose target isn't loaded, so somebody should go and ask for it37
+ pub(crate) missing_replies: Vec<OwnedEventId>,38
+}39
+40
+pub(crate) struct NormalisedTimelineDiffs {41
+ pub(crate) diffs: Vec<TimelineDiff>,42
+ pub(crate) media: Vec<MediaRegistration>,43
+ pub(crate) missing_replies: Vec<OwnedEventId>,44
+}45
+46
+struct Normaliser<'a> {47
+ current_user_id: Option<&'a UserId>,48
+ scope: &'a MediaScope,49
+ media: Vec<MediaRegistration>,50
+ missing_replies: Vec<OwnedEventId>,51
+}52
+53
+impl Normaliser<'_> {54
+ fn item(&mut self, item: &SdkTimelineItem) -> TimelineItem {55
+ if let Some(media) = media_registration_for_item(item, self.scope) {56
+ self.media.push(media);57
+ }58
+ match item.kind() {59
+ TimelineItemKind::Event(event) => {60
+ TimelineItem::Event(Box::new(self.event(item, event)))61
+ }62
+ TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(timestamp)) => {63
+ TimelineItem::DayDivider {64
+ timestamp_ms: timestamp.get().into(),65
+ }66
+ }67
+ TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => TimelineItem::ReadMarker,68
+ TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {69
+ TimelineItem::TimelineStart70
+ }71
+ }72
+ }73
+74
+ fn event(&mut self, item: &SdkTimelineItem, event: &EventTimelineItem) -> TimelineEvent {75
+ let msg_like = match event.content() {76
+ TimelineItemContent::MsgLike(content) => Some(content),77
+ _ => None,78
+ };79
+ let reply_to = msg_like80
+ .and_then(|content| content.in_reply_to.as_ref())81
+ .map(|reply| {82
+ let details = match &reply.event {83
+ TimelineDetails::Ready(embedded) => ReplyDetails::Ready {84
+ sender: sender(&embedded.sender, &embedded.sender_profile),85
+ content: Box::new(content(&embedded.content, None)),86
+ timestamp_ms: embedded.timestamp.get().into(),87
+ },88
+ TimelineDetails::Unavailable => {89
+ self.missing_replies.push(reply.event_id.clone());90
+ ReplyDetails::Pending91
+ }92
+ TimelineDetails::Pending => ReplyDetails::Pending,93
+ TimelineDetails::Error(_) => ReplyDetails::Unavailable,94
+ };95
+ ReplyReference {96
+ event_id: reply.event_id.clone(),97
+ details,98
+ }99
+ });100
+ let mut read_by: Vec<_> = event101
+ .read_receipts()102
+ .keys()103
+ .filter(|user_id| *user_id != event.sender())104
+ .cloned()105
+ .collect();106
+ read_by.sort();107
+ TimelineEvent {108
+ item_id: TimelineItemId::new(item.unique_id().0.clone()),109
+ event_id: event.event_id().map(ToOwned::to_owned),110
+ transaction_id: event.transaction_id().map(ToOwned::to_owned),111
+ sender: sender(event.sender(), event.sender_profile()),112
+ timestamp_ms: event.timestamp().get().into(),113
+ own: event.is_own(),114
+ send_state: send_state(event.send_state()),115
+ content: content(event.content(), Some(&media_identity(item, self.scope))),116
+ reply_to,117
+ thread_root: msg_like.and_then(|content| content.thread_root.clone()),118
+ thread_summary: msg_like.and_then(thread_summary),119
+ reactions: reactions(event, self.current_user_id),120
+ read_by,121
+ editable: event.is_editable(),122
+ }123
+ }29124
}30125
31126
pub(crate) fn normalise_timeline<'a>(@@ -33,19 +128,18 @@ pub(crate) fn normalise_timeline<'a>(
33128
current_user_id: Option<&UserId>,34129
scope: &MediaScope,35130
) -> NormalisedTimeline {36
- let mut normalised = NormalisedTimeline {37
- items: Vec::new(),131
+ let mut normaliser = Normaliser {132
+ current_user_id,133
+ scope,38134
media: Vec::new(),135
+ missing_replies: Vec::new(),39136
};40
- for item in items {41
- if let Some(media) = media_registration_for_item(item, scope) {42
- normalised.media.push(media);43
- }44
- normalised45
- .items46
- .push(normalise_timeline_item(item, current_user_id, scope));137
+ let items = items.map(|item| normaliser.item(item)).collect();138
+ NormalisedTimeline {139
+ items,140
+ media: normaliser.media,141
+ missing_replies: normaliser.missing_replies,47142
}48
- normalised49143
}50144
51145
pub(crate) fn normalise_timeline_diffs(@@ -53,171 +147,265 @@ pub(crate) fn normalise_timeline_diffs(
53147
current_user_id: Option<&UserId>,54148
scope: &MediaScope,55149
) -> NormalisedTimelineDiffs {56
- let mut media = Vec::new();150
+ let mut normaliser = Normaliser {151
+ current_user_id,152
+ scope,153
+ media: Vec::new(),154
+ missing_replies: Vec::new(),155
+ };57156
let diffs = diffs58157
.into_iter()59
- .map(|diff| {60
- let diff = diff.map(|item| {61
- if let Some(registration) = media_registration_for_item(&item, scope) {62
- media.push(registration);63
- }64
- normalise_timeline_item(&item, current_user_id, scope)65
- });66
- match diff {67
- VectorDiff::Append { values } => TimelineDiff::Append {68
- values: values.into_iter().collect(),69
- },70
- VectorDiff::Clear => TimelineDiff::Clear,71
- VectorDiff::PushFront { value } => TimelineDiff::PushFront { value },72
- VectorDiff::PushBack { value } => TimelineDiff::PushBack { value },73
- VectorDiff::PopFront => TimelineDiff::PopFront,74
- VectorDiff::PopBack => TimelineDiff::PopBack,75
- VectorDiff::Insert { index, value } => TimelineDiff::Insert { index, value },76
- VectorDiff::Set { index, value } => TimelineDiff::Set { index, value },77
- VectorDiff::Remove { index } => TimelineDiff::Remove { index },78
- VectorDiff::Truncate { length } => TimelineDiff::Truncate { length },79
- VectorDiff::Reset { values } => TimelineDiff::Reset {80
- values: values.into_iter().collect(),81
- },82
- }158
+ .map(|diff| match diff.map(|item| normaliser.item(&item)) {159
+ VectorDiff::Append { values } => TimelineDiff::Append {160
+ values: values.into_iter().collect(),161
+ },162
+ VectorDiff::Clear => TimelineDiff::Clear,163
+ VectorDiff::PushFront { value } => TimelineDiff::PushFront { value },164
+ VectorDiff::PushBack { value } => TimelineDiff::PushBack { value },165
+ VectorDiff::PopFront => TimelineDiff::PopFront,166
+ VectorDiff::PopBack => TimelineDiff::PopBack,167
+ VectorDiff::Insert { index, value } => TimelineDiff::Insert { index, value },168
+ VectorDiff::Set { index, value } => TimelineDiff::Set { index, value },169
+ VectorDiff::Remove { index } => TimelineDiff::Remove { index },170
+ VectorDiff::Truncate { length } => TimelineDiff::Truncate { length },171
+ VectorDiff::Reset { values } => TimelineDiff::Reset {172
+ values: values.into_iter().collect(),173
+ },83174
})84175
.collect();85
- NormalisedTimelineDiffs { diffs, media }176
+ NormalisedTimelineDiffs {177
+ diffs,178
+ media: normaliser.media,179
+ missing_replies: normaliser.missing_replies,180
+ }86181
}87182
88
-pub(crate) struct NormalisedTimelineDiffs {89
- pub(crate) diffs: Vec<TimelineDiff>,90
- pub(crate) media: Vec<MediaRegistration>,183
+/// `media_identity` is `None` for embedded reply content, which never gets fetchable media of its own184
+fn content(content: &TimelineItemContent, media_identity: Option<&str>) -> TimelineContent {185
+ match content {186
+ TimelineItemContent::MsgLike(content) => msg_like(content, media_identity),187
+ TimelineItemContent::MembershipChange(change) => TimelineContent::Membership {188
+ user_id: change.user_id().to_owned(),189
+ display_name: change.display_name(),190
+ change: change191
+ .change()192
+ .map_or(MembershipChange::Other, membership_change),193
+ },194
+ TimelineItemContent::ProfileChange(change) => TimelineContent::Profile {195
+ user_id: change.user_id().to_owned(),196
+ display_name: change.displayname_change().map(|change| Change {197
+ old: change.old.clone(),198
+ new: change.new.clone(),199
+ }),200
+ avatar: change.avatar_url_change().map(|change| Change {201
+ old: change.old.clone(),202
+ new: change.new.clone(),203
+ }),204
+ },205
+ TimelineItemContent::OtherState(state) => match state.content() {206
+ AnyOtherStateEventContentChange::RoomName(change) => {207
+ TimelineContent::RoomName(original(change).map(|content| content.name.clone()))208
+ }209
+ AnyOtherStateEventContentChange::RoomTopic(change) => {210
+ TimelineContent::RoomTopic(original(change).map(|content| content.topic.clone()))211
+ }212
+ AnyOtherStateEventContentChange::RoomAvatar(change) => TimelineContent::RoomAvatar(213
+ original(change).and_then(|content| content.url.clone()),214
+ ),215
+ AnyOtherStateEventContentChange::RoomEncryption(_) => {216
+ TimelineContent::EncryptionEnabled217
+ }218
+ AnyOtherStateEventContentChange::RoomCreate(_) => TimelineContent::RoomCreated,219
+ AnyOtherStateEventContentChange::RoomTombstone(change) => match original(change) {220
+ Some(content) => TimelineContent::RoomUpgraded {221
+ successor: content.replacement_room.clone(),222
+ },223
+ None => other_state(state.content().event_type().to_string(), state.state_key()),224
+ },225
+ other => other_state(other.event_type().to_string(), state.state_key()),226
+ },227
+ TimelineItemContent::FailedToParseMessageLike { event_type, .. } => {228
+ TimelineContent::Other {229
+ event_type: event_type.to_string(),230
+ }231
+ }232
+ TimelineItemContent::FailedToParseState {233
+ event_type,234
+ state_key,235
+ ..236
+ } => other_state(event_type.to_string(), state_key),237
+ TimelineItemContent::CallInvite => TimelineContent::Other {238
+ event_type: "m.call.invite".to_owned(),239
+ },240
+ TimelineItemContent::RtcNotification { .. } => TimelineContent::Other {241
+ event_type: "org.matrix.msc4075.rtc.notification".to_owned(),242
+ },243
+ }91244
}92245
93
-fn normalise_timeline_item(94
- item: &SdkTimelineItem,95
- current_user_id: Option<&UserId>,96
- scope: &MediaScope,97
-) -> TimelineItem {98
- match item.kind() {99
- TimelineItemKind::Event(event) => TimelineItem::Message(Box::new(timeline_message(100
- item,101
- event,102
- current_user_id,103
- scope,104
- ))),105
- TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(timestamp)) => {106
- TimelineItem::DayDivider {107
- timestamp_ms: timestamp.get().into(),246
+fn other_state(event_type: String, state_key: &str) -> TimelineContent {247
+ TimelineContent::OtherState {248
+ event_type,249
+ state_key: state_key.to_owned(),250
+ }251
+}252
+253
+fn original<C>(change: &StateEventContentChange<C>) -> Option<&C>254
+where255
+ C: ruma::events::StaticStateEventContent + ruma::events::RedactContent,256
+{257
+ match change {258
+ StateEventContentChange::Original { content, .. } => Some(content),259
+ StateEventContentChange::Redacted(_) => None,260
+ }261
+}262
+263
+fn msg_like(content: &MsgLikeContent, media_identity: Option<&str>) -> TimelineContent {264
+ match &content.kind {265
+ MsgLikeKind::Message(message) => TimelineContent::Message(Message {266
+ kind: message_kind(message.msgtype()),267
+ body: message.body().to_owned(),268
+ formatted_body: formatted_body(message.msgtype()),269
+ mentions: message.mentions().map(|mentions| MessageMentions {270
+ user_ids: mentions.user_ids.iter().cloned().collect(),271
+ room: mentions.room,272
+ }),273
+ edited: message.is_edited(),274
+ media: media_identity275
+ .and_then(|identity| media_from_message(identity, message.msgtype())),276
+ }),277
+ MsgLikeKind::Sticker(sticker) => {278
+ let sticker = sticker.content();279
+ match media_identity {280
+ Some(identity) => TimelineContent::Sticker {281
+ body: sticker.body.clone(),282
+ media: media_descriptor_for_sticker(identity, sticker),283
+ },284
+ None => TimelineContent::Other {285
+ event_type: "m.sticker".to_owned(),286
+ },108287
}109288
}110
- TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => TimelineItem::ReadMarker,111
- TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {112
- TimelineItem::TimelineStart289
+ MsgLikeKind::Poll(poll) => TimelineContent::Poll {290
+ question: poll.results().question,291
+ },292
+ MsgLikeKind::Redacted => TimelineContent::Redacted,293
+ MsgLikeKind::UnableToDecrypt(encrypted) => {294
+ TimelineContent::UnableToDecrypt(match encrypted {295
+ EncryptedMessage::MegolmV1AesSha2 { cause, .. } => utd_cause(*cause),296
+ _ => UtdCause::Unknown,297
+ })113298
}299
+ MsgLikeKind::Other(other) => TimelineContent::Other {300
+ event_type: other.event_type().to_string(),301
+ },302
+ MsgLikeKind::LiveLocation(_) => TimelineContent::Other {303
+ event_type: "m.beacon_info".to_owned(),304
+ },114305
}115306
}116307
117
-fn timeline_message(118
- item: &SdkTimelineItem,119
- event: &matrix_sdk_ui::timeline::EventTimelineItem,120
- current_user_id: Option<&UserId>,121
- scope: &MediaScope,122
-) -> TimelineMessage {123
- let (body, formatted_body, mentions, edited, unable_to_decrypt, media) = match event.content() {124
- TimelineItemContent::MsgLike(content) => match &content.kind {125
- MsgLikeKind::Message(message) => (126
- message.body().to_owned(),127
- formatted_body(message.msgtype()),128
- message.mentions().map(message_mentions),129
- message.is_edited(),130
- false,131
- media_from_message(&media_identity(item, scope), message.msgtype()),132
- ),133
- MsgLikeKind::Sticker(sticker) => {134
- let content = sticker.content();135
- (136
- content.body.clone(),137
- None,138
- None,139
- false,140
- false,141
- Some(media_descriptor_for_sticker(142
- &media_identity(item, scope),143
- content,144
- )),145
- )146
- }147
- MsgLikeKind::Redacted => ("<message redacted>".into(), None, None, false, false, None),148
- MsgLikeKind::UnableToDecrypt(_) => (149
- "<unable to decrypt message>".into(),150
- None,151
- None,152
- false,153
- true,154
- None,155
- ),156
- MsgLikeKind::Poll(_) => ("[poll]".into(), None, None, false, false, None),157
- MsgLikeKind::LiveLocation(_) => {158
- ("[live location]".into(), None, None, false, false, None)159
- }160
- MsgLikeKind::Other(other) => (161
- format!("[{}]", other.event_type()),162
- None,163
- None,164
- false,165
- false,166
- None,167
- ),168
- },169
- other => (170
- format!(171
- "[{}]",172
- other.event_type_str().unwrap_or_else(|| "event".into())173
- ),174
- None,175
- None,176
- false,177
- false,178
- None,179
- ),180
- };181
- let reply_to = match event.content() {182
- TimelineItemContent::MsgLike(content) => content.in_reply_to.as_ref().map(|reply| {183
- let event_id = reply.event_id.to_owned();184
- let embedded_sender = match &reply.event {185
- TimelineDetails::Ready(event) => Some((186
- event.sender.clone(),187
- sender_name(&event.sender, &event.sender_profile),188
- )),189
- _ => None,190
- };191
- ReplyReference {192
- event_id,193
- sender_id: embedded_sender.as_ref().map(|(id, _)| id.clone()),194
- sender_name: embedded_sender.map(|(_, name)| name),195
- }196
- }),308
+fn message_kind(message: &MessageType) -> MessageKind {309
+ match message {310
+ MessageType::Text(_) => MessageKind::Text,311
+ MessageType::Emote(_) => MessageKind::Emote,312
+ MessageType::Notice(_) => MessageKind::Notice,313
+ MessageType::ServerNotice(_) => MessageKind::ServerNotice,314
+ MessageType::Image(_) => MessageKind::Image,315
+ MessageType::Video(_) => MessageKind::Video,316
+ MessageType::Audio(_) => MessageKind::Audio,317
+ MessageType::File(_) => MessageKind::File,318
+ MessageType::Location(_) => MessageKind::Location,319
+ MessageType::VerificationRequest(_) => MessageKind::VerificationRequest,320
+ other => MessageKind::Other(other.msgtype().to_owned()),321
+ }322
+}323
+324
+const fn membership_change(change: SdkMembershipChange) -> MembershipChange {325
+ match change {326
+ SdkMembershipChange::Joined => MembershipChange::Joined,327
+ SdkMembershipChange::Left => MembershipChange::Left,328
+ SdkMembershipChange::Banned => MembershipChange::Banned,329
+ SdkMembershipChange::Unbanned => MembershipChange::Unbanned,330
+ SdkMembershipChange::Kicked => MembershipChange::Kicked,331
+ SdkMembershipChange::KickedAndBanned => MembershipChange::KickedAndBanned,332
+ SdkMembershipChange::Invited => MembershipChange::Invited,333
+ SdkMembershipChange::InvitationAccepted => MembershipChange::InvitationAccepted,334
+ SdkMembershipChange::InvitationRejected => MembershipChange::InvitationRejected,335
+ SdkMembershipChange::InvitationRevoked => MembershipChange::InvitationRevoked,336
+ SdkMembershipChange::Knocked => MembershipChange::Knocked,337
+ SdkMembershipChange::KnockAccepted => MembershipChange::KnockAccepted,338
+ SdkMembershipChange::KnockRetracted => MembershipChange::KnockRetracted,339
+ SdkMembershipChange::KnockDenied => MembershipChange::KnockDenied,340
+ SdkMembershipChange::None341
+ | SdkMembershipChange::Error342
+ | SdkMembershipChange::NotImplemented => MembershipChange::Other,343
+ }344
+}345
+346
+const fn utd_cause(cause: SdkUtdCause) -> UtdCause {347
+ match cause {348
+ SdkUtdCause::Unknown => UtdCause::Unknown,349
+ SdkUtdCause::SentBeforeWeJoined => UtdCause::SentBeforeWeJoined,350
+ SdkUtdCause::VerificationViolation => UtdCause::VerificationViolation,351
+ SdkUtdCause::UnsignedDevice => UtdCause::UnsignedDevice,352
+ SdkUtdCause::UnknownDevice => UtdCause::UnknownDevice,353
+ SdkUtdCause::HistoricalMessageAndBackupIsDisabled => {354
+ UtdCause::HistoricalMessageAndBackupIsDisabled355
+ }356
+ SdkUtdCause::WithheldForUnverifiedOrInsecureDevice => {357
+ UtdCause::WithheldForUnverifiedOrInsecureDevice358
+ }359
+ SdkUtdCause::WithheldBySender => UtdCause::WithheldBySender,360
+ SdkUtdCause::HistoricalMessageAndDeviceIsUnverified => {361
+ UtdCause::HistoricalMessageAndDeviceIsUnverified362
+ }363
+ }364
+}365
+366
+fn sender(user_id: &UserId, profile: &TimelineDetails<Profile>) -> Sender {367
+ let profile = match profile {368
+ TimelineDetails::Ready(profile) => Some(profile),197369
_ => None,198370
};199
- let (thread_root, thread_summary) = match event.content() {200
- TimelineItemContent::MsgLike(content) => (201
- content.thread_root.clone(),202
- content.thread_summary.as_ref().map(|summary| {203
- let latest_event = match &summary.latest_event {204
- TimelineDetails::Ready(event) => Some(event.as_ref()),205
- _ => None,206
- };207
- ThreadSummary {208
- reply_count: summary.num_replies,209
- latest_event_id: latest_event.and_then(|event| match &event.identifier {210
- TimelineEventItemId::EventId(event_id) => Some(event_id.clone()),211
- TimelineEventItemId::TransactionId(_) => None,212
- }),213
- latest_event_timestamp_ms: latest_event214
- .map(|event| event.timestamp.get().into()),215
- }371
+ Sender {372
+ user_id: user_id.to_owned(),373
+ display_name: profile.and_then(|profile| profile.display_name.clone()),374
+ avatar: profile.and_then(|profile| profile.avatar_url.clone()),375
+ name_ambiguous: profile.is_some_and(|profile| profile.display_name_ambiguous),376
+ }377
+}378
+379
+fn send_state(state: Option<&EventSendState>) -> SendState {380
+ match state {381
+ None | Some(EventSendState::Sent { .. }) => SendState::Sent,382
+ Some(EventSendState::NotSentYet { progress: Some(_) }) => SendState::Sending,383
+ Some(EventSendState::NotSentYet { progress: None }) => SendState::Queued,384
+ Some(EventSendState::SendingFailed { is_recoverable, .. }) => SendState::Failed {385
+ retriable: *is_recoverable,386
+ },387
+ }388
+}389
+390
+fn thread_summary(content: &MsgLikeContent) -> Option<ThreadSummary> {391
+ content.thread_summary.as_ref().map(|summary| {392
+ let latest_event = match &summary.latest_event {393
+ TimelineDetails::Ready(event) => Some(event.as_ref()),394
+ _ => None,395
+ };396
+ ThreadSummary {397
+ reply_count: summary.num_replies,398
+ latest_event_id: latest_event.and_then(|event| match &event.identifier {399
+ TimelineEventItemId::EventId(event_id) => Some(event_id.clone()),400
+ TimelineEventItemId::TransactionId(_) => None,216401
}),217
- ),218
- _ => (None, None),219
- };220
- let reactions = event402
+ latest_event_timestamp_ms: latest_event.map(|event| event.timestamp.get().into()),403
+ }404
+ })405
+}406
+407
+fn reactions(event: &EventTimelineItem, current_user_id: Option<&UserId>) -> Vec<Reaction> {408
+ event221409
.content()222410
.reactions()223411
.into_iter()@@ -228,56 +416,7 @@ fn timeline_message(
228416
own: current_user_id.is_some_and(|user_id| senders.contains_key(user_id)),229417
senders: senders.keys().cloned().collect(),230418
})231
- .collect();232
- let mut read_by: Vec<_> = event233
- .read_receipts()234
- .keys()235
- .filter(|user_id| *user_id != event.sender())236
- .cloned()237
- .collect();238
- read_by.sort();239
-240
- TimelineMessage {241
- item_id: TimelineItemId::new(item.unique_id().0.clone()),242
- event_id: event.event_id().map(ToOwned::to_owned),243
- transaction_id: event.transaction_id().map(ToOwned::to_owned),244
- sender_id: event.sender().to_owned(),245
- sender_name: sender_name(event.sender(), event.sender_profile()),246
- body,247
- formatted_body,248
- mentions,249
- thread_root,250
- thread_summary,251
- timestamp_ms: event.timestamp().get().into(),252
- own: event.is_own(),253
- send_state: match event.send_state() {254
- None | Some(EventSendState::Sent { .. }) => SendState::Sent,255
- Some(EventSendState::NotSentYet { progress: Some(_) }) => SendState::Sending,256
- Some(EventSendState::NotSentYet { progress: None }) => SendState::Queued,257
- Some(EventSendState::SendingFailed { is_recoverable, .. }) => SendState::Failed {258
- retriable: *is_recoverable,259
- },260
- },261
- edited,262
- unable_to_decrypt,263
- media,264
- reply_to,265
- reactions,266
- read_by,267
- }268
-}269
-270
-fn sender_name(271
- sender: &UserId,272
- sender_profile: &TimelineDetails<matrix_sdk_ui::timeline::Profile>,273
-) -> String {274
- match sender_profile {275
- TimelineDetails::Ready(profile) => profile276
- .display_name277
- .clone()278
- .unwrap_or_else(|| sender.to_string()),279
- _ => sender.to_string(),280
- }419
+ .collect()281420
}282421
283422
fn formatted_body(message: &MessageType) -> Option<String> {@@ -293,10 +432,3 @@ fn formatted_body(message: &MessageType) -> Option<String> {
293432
}?;294433
(formatted.format == MessageFormat::Html).then(|| formatted.body.clone())295434
}296
-297
-fn message_mentions(mentions: &ruma::events::Mentions) -> MessageMentions {298
- MessageMentions {299
- user_ids: mentions.user_ids.iter().cloned().collect(),300
- room: mentions.room,301
- }302
-}@@ -27,7 +27,7 @@ use tokio::sync::{OnceCell, mpsc, oneshot, watch};
2727
2828
use crate::{2929
AccountCommand, AttachmentUpload, Command, CommandOutput, ConnectionState, CreateRoomOptions,30
- EncryptionCommand, EncryptionState, Error, ErrorKind, Event, InteractiveAuth,30
+ EncryptionCommand, EncryptionState, Error, ErrorKind, Event, EventRef, InteractiveAuth,3131
InteractiveAuthOutcome, LifecycleCommand, Media, MediaCommand, MediaId, MediaSize,3232
MemberFilter, MemberSummary, MessageContent, MessageEditContent, NotificationCommand,3333
OwnDevice, PowerLevelChanges, Profile, PushNotification, PushNotificationEvent,@@ -303,12 +303,12 @@ impl ClientHandle {
303303
pub async fn edit_message(304304
&self,305305
timeline_id: TimelineId,306
- event_id: OwnedEventId,306
+ target: impl Into<EventRef>,307307
content: MessageEditContent,308308
) -> Result<(), Error> {309309
self.run(Command::Timeline(TimelineCommand::Edit {310310
timeline_id,311
- event_id,311
+ target: target.into(),312312
content,313313
}))314314
.await@@ -317,12 +317,12 @@ impl ClientHandle {
317317
pub async fn redact(318318
&self,319319
timeline_id: TimelineId,320
- event_id: OwnedEventId,320
+ target: impl Into<EventRef>,321321
reason: Option<String>,322322
) -> Result<(), Error> {323323
self.run(Command::Timeline(TimelineCommand::Redact {324324
timeline_id,325
- event_id,325
+ target: target.into(),326326
reason,327327
}))328328
.await@@ -331,12 +331,12 @@ impl ClientHandle {
331331
pub async fn toggle_reaction(332332
&self,333333
timeline_id: TimelineId,334
- event_id: OwnedEventId,334
+ target: impl Into<EventRef>,335335
key: impl Into<String>,336336
) -> Result<(), Error> {337337
self.run(Command::Timeline(TimelineCommand::ToggleReaction {338338
timeline_id,339
- event_id,339
+ target: target.into(),340340
key: key.into(),341341
}))342342
.await@@ -38,7 +38,7 @@ async fn empty_message_bodies_fail_with_empty_body() {
3838
},3939
TimelineCommand::Edit {4040
timeline_id: room_id.to_owned().into(),41
- event_id: event_id!("$edit:localhost").to_owned(),41
+ target: event_id!("$edit:localhost").to_owned().into(),4242
content: MessageEditContent::text("\t"),4343
},4444
];@@ -91,12 +91,12 @@ async fn opens_a_room_and_sends_messages() {
9191
let media_event = receive_until(&mut events, |event| {9292
event_messages(event)9393
.into_iter()94
- .any(|message| message.media.is_some())94
+ .any(|event| support::media_of(event).is_some())9595
})9696
.await;9797
let media_id = event_messages(&media_event)9898
.into_iter()99
- .find_map(|message| message.media.as_ref().map(|media| media.id.clone()))99
+ .find_map(|event| support::media_of(event).map(|media| media.id.clone()))100100
.unwrap();101101
let media = handle.media(&media_id, MediaSize::Full).await.unwrap();102102
assert!(!media.bytes.is_empty());@@ -164,13 +164,15 @@ async fn keeps_open_room_timelines_independent() {
164164
let event = events.recv().await.expect("event stream closed");165165
if let Event::TimelineChanged { timeline_id, .. } = &event {166166
first_updated |= timeline_id.room_id() == first_id167
- && event_messages(&event)168
- .into_iter()169
- .any(|message| message.body == "first update");167
+ && event_messages(&event).into_iter().any(|event| {168
+ support::message(event)169
+ .is_some_and(|message| message.body == "first update")170
+ });170171
second_updated |= timeline_id.room_id() == second_id171
- && event_messages(&event)172
- .into_iter()173
- .any(|message| message.body == "second update");172
+ && event_messages(&event).into_iter().any(|event| {173
+ support::message(event)174
+ .is_some_and(|message| message.body == "second update")175
+ });174176
}175177
}176178
})@@ -187,9 +189,9 @@ async fn keeps_open_room_timelines_independent() {
187189
.await;188190
receive_until(&mut events, |event| {189191
matches!(event, Event::TimelineChanged { timeline_id, .. } if timeline_id.room_id() == second_id)190
- && event_messages(event)191
- .into_iter()192
- .any(|message| message.body == "still live")192
+ && event_messages(event).into_iter().any(|event| {193
+ support::message(event).is_some_and(|message| message.body == "still live")194
+ })193195
})194196
.await;195197
@@ -228,16 +230,16 @@ async fn resync_replaces_timeline_before_ack_and_preserves_following_diffs() {
228230
)229231
.await;230232
receive_until(&mut events, |event| {231
- event_messages(event)232
- .into_iter()233
- .any(|message| message.body == "before reset")233
+ event_messages(event).into_iter().any(|event| {234
+ support::message(event).is_some_and(|message| message.body == "before reset")235
+ })234236
})235237
.await;236238
237239
let mut items =238240
resync_and_confirm_reset_first(&handle, &mut events, room_id.to_owned().into()).await;239241
assert!(items.iter().any(|item| matches!(item,240
- TimelineItem::Message(message) if message.body == "before reset"242
+ TimelineItem::Event(event) if support::message(event).is_some_and(|message| message.body == "before reset")241243
)));242244
243245
server@@ -259,7 +261,7 @@ async fn resync_replaces_timeline_before_ack_and_preserves_following_diffs() {
259261
}260262
if items.iter().any(|item| {261263
matches!(item,262
- TimelineItem::Message(message) if message.body == "after reset"264
+ TimelineItem::Event(event) if support::message(event).is_some_and(|message| message.body == "after reset")263265
)264266
}) {265267
break;@@ -462,9 +464,9 @@ async fn pagination_reports_and_remembers_start_of_history() {
462464
.await;463465
receive_until(&mut events, |event| {464466
matches!(event, Event::TimelineChanged { timeline_id: changed, .. } if changed.room_id() == room_id)465
- && event_messages(event)466
- .into_iter()467
- .any(|message| message.body == "latest")467
+ && event_messages(event).into_iter().any(|event| {468
+ support::message(event).is_some_and(|message| message.body == "latest")469
+ })468470
})469471
.await;470472
@@ -292,20 +292,17 @@ async fn queued_offline_attachment_exposes_fetchable_local_media() {
292292
});293293
harness.handle.upload(upload).await.unwrap();294294
let event = support::receive_until(&mut harness.events, |event| {295
- support::event_messages(event).into_iter().any(|message| {296
- message.own297
- && message.transaction_id.is_some()298
- && message299
- .media300
- .as_ref()301
- .is_some_and(|media| media.filename == "pending.png")295
+ support::event_messages(event).into_iter().any(|event| {296
+ event.own297
+ && event.transaction_id.is_some()298
+ && support::media_of(event).is_some_and(|media| media.filename == "pending.png")302299
})303300
})304301
.await;305302
let media = support::event_messages(&event)306303
.into_iter()307
- .find(|message| message.own && message.media.is_some())308
- .and_then(|message| message.media.as_ref())304
+ .find(|event| event.own && support::media_of(event).is_some())305
+ .and_then(support::media_of)309306
.unwrap();310307
assert_eq!((media.width, media.height), (Some(32), Some(16)));311308
assert_eq!(media.size_bytes, Some(original.len() as u64));@@ -714,12 +711,12 @@ async fn start_with_config(content: RoomMessageEventContent, config: EngineConfi
714711
let event = support::receive_until(&mut events, |event| {715712
support::event_messages(event)716713
.into_iter()717
- .any(|message| message.media.is_some())714
+ .any(|event| support::media_of(event).is_some())718715
})719716
.await;720717
let media_id = support::event_messages(&event)721718
.into_iter()722
- .find_map(|message| message.media.as_ref().map(|media| media.id.clone()))719
+ .find_map(|event| support::media_of(event).map(|media| media.id.clone()))723720
.unwrap();724721
725722
MediaHarness {@@ -53,12 +53,12 @@ async fn delayed_media_fetch_does_not_block_timeline_command() {
5353
let media_event = receive_until(&mut events, |event| {5454
event_messages(event)5555
.into_iter()56
- .any(|message| message.media.is_some())56
+ .any(|event| support::media_of(event).is_some())5757
})5858
.await;5959
let media_id = event_messages(&media_event)6060
.into_iter()61
- .find_map(|message| message.media.as_ref().map(|media| media.id.clone()))61
+ .find_map(|event| support::media_of(event).map(|media| media.id.clone()))6262
.unwrap();6363
6464
let delayed_image = || {@@ -175,8 +175,10 @@ async fn queue_message(
175175
let event = events.recv().await.expect("event stream closed");176176
let transaction = support::event_messages(&event)177177
.into_iter()178
- .find(|message| message.body == body)179
- .and_then(|message| message.transaction_id.clone());178
+ .find(|event| {179
+ support::message(event).is_some_and(|message| message.body == body)180
+ })181
+ .and_then(|event| event.transaction_id.clone());180182
if let Some(transaction) = transaction {181183
return transaction;182184
}@@ -2,7 +2,10 @@
22
33
use std::time::Duration;44
5
-use macltok::{ClientHandle, Event, EventStream, TimelineDiff, TimelineItem, TimelineMessage};5
+use macltok::{6
+ ClientHandle, Event, EventStream, MediaDescriptor, Message, TimelineContent, TimelineDiff,7
+ TimelineEvent, TimelineItem,8
+};69
710
pub async fn receive_until(events: &mut EventStream, predicate: impl Fn(&Event) -> bool) -> Event {811
tokio::time::timeout(Duration::from_secs(5), async {@@ -30,7 +33,7 @@ pub async fn shut_down(
3033
running.await.unwrap().unwrap();3134
}3235
33
-pub fn event_messages(event: &Event) -> Vec<&TimelineMessage> {36
+pub fn event_messages(event: &Event) -> Vec<&TimelineEvent> {3437
let items = match event {3538
Event::TimelineReset { items, .. } => items.iter().collect::<Vec<_>>(),3639
Event::TimelineChanged { diffs, .. } => diffs.iter().flat_map(diff_items).collect(),@@ -39,12 +42,27 @@ pub fn event_messages(event: &Event) -> Vec<&TimelineMessage> {
3942
items4043
.into_iter()4144
.filter_map(|item| match item {42
- TimelineItem::Message(message) => Some(message.as_ref()),45
+ TimelineItem::Event(event) => Some(event.as_ref()),4346
_ => None,4447
})4548
.collect()4649
}4750
51
+pub fn message(event: &TimelineEvent) -> Option<&Message> {52
+ match &event.content {53
+ TimelineContent::Message(message) => Some(message),54
+ _ => None,55
+ }56
+}57
+58
+pub fn media_of(event: &TimelineEvent) -> Option<&MediaDescriptor> {59
+ match &event.content {60
+ TimelineContent::Message(message) => message.media.as_ref(),61
+ TimelineContent::Sticker { media, .. } => Some(media),62
+ _ => None,63
+ }64
+}65
+4866
pub fn diff_items(diff: &TimelineDiff) -> Vec<&TimelineItem> {4967
match diff {5068
TimelineDiff::Append { values } | TimelineDiff::Reset { values } => values.iter().collect(),