Commit 89bc9f8d246d
tarball6 files changed+357-2
@@ -0,0 +1,224 @@
1
+use std::sync::Arc;2
+3
+use matrix_sdk::{room::MessagesOptions, Room};4
+use matrix_sdk_ui::timeline::{RoomExt, TimelineEventFocusThreadMode, TimelineFocus};5
+use ruma::{uint, OwnedEventId};6
+7
+use crate::{8
+ normalise::{normalise_timeline, MediaScope},9
+ Error, ErrorKind, EventContext, EventContextCommand, EventContextDirection, EventContextPage,10
+ TimelineItem,11
+};12
+13
+const MAX_CONTEXT_LIMIT: u16 = 50;14
+15
+pub(crate) async fn execute(16
+ room: Room,17
+ current_user_id: Option<ruma::OwnedUserId>,18
+ command: EventContextCommand,19
+) -> Result<crate::CommandOutput, Error> {20
+ if let EventContextCommand::Before { token, limit, .. }21
+ | EventContextCommand::After { token, limit, .. } = &command22
+ {23
+ validate_limit(*limit)?;24
+ if token.is_empty() {25
+ return Err(Error::new(26
+ ErrorKind::Invalid,27
+ "context pagination token is empty",28
+ ));29
+ }30
+ return execute_page(31
+ room,32
+ current_user_id,33
+ token,34
+ *limit,35
+ matches!(command, EventContextCommand::Before { .. }),36
+ )37
+ .await;38
+ }39
+40
+ let (event_id, limit) = match &command {41
+ EventContextCommand::Fetch {42
+ event_id,43
+ before_limit,44
+ after_limit,45
+ ..46
+ } => {47
+ validate_limit(*before_limit)?;48
+ validate_limit(*after_limit)?;49
+ (event_id.clone(), (*before_limit).max(*after_limit))50
+ }51
+ EventContextCommand::Before { .. } | EventContextCommand::After { .. } => unreachable!(),52
+ };53
+ validate_limit(limit)?;54
+55
+ let raw_context = if matches!(command, EventContextCommand::Fetch { .. }) {56
+ Some(57
+ room.event_with_context(&event_id, false, uint!(0), None)58
+ .await59
+ .map_err(|source| {60
+ let error = Error::from_sdk("fetching event context failed", source);61
+ if error62
+ .server63
+ .as_ref()64
+ .is_some_and(|server| server.status == 404)65
+ {66
+ Error::new(ErrorKind::EventNotFound, "event was not found")67
+ } else {68
+ error69
+ }70
+ })?,71
+ )72
+ } else {73
+ None74
+ };75
+76
+ let focus = TimelineFocus::Event {77
+ target: event_id.clone(),78
+ num_context_events: limit,79
+ thread_mode: TimelineEventFocusThreadMode::Automatic {80
+ hide_threaded_events: true,81
+ },82
+ };83
+ let timeline = room84
+ .timeline_builder()85
+ .with_focus(focus)86
+ .build()87
+ .await88
+ .map_err(|source| {89
+ Error::with_source(ErrorKind::Timeline, "building event context failed", source)90
+ })?;91
+92
+ if let EventContextCommand::Fetch { .. } = command {93
+ let raw = raw_context.expect("fetch context response");94
+ let normalized = normalize_items(timeline.items().await, current_user_id.as_deref());95
+ let (before, event, after) = split_items(normalized, &event_id);96
+ if event.is_none() && raw.event.is_none() {97
+ return Err(Error::new(ErrorKind::EventNotFound, "event was not found"));98
+ }99
+ return Ok(crate::CommandOutput::EventContext(EventContext {100
+ events_before: before,101
+ event,102
+ events_after: after,103
+ start_token: raw.prev_batch_token,104
+ end_token: raw.next_batch_token,105
+ }));106
+ }107
+108
+ unreachable!("fetch context returned above")109
+}110
+111
+async fn execute_page(112
+ room: Room,113
+ current_user_id: Option<ruma::OwnedUserId>,114
+ token: &str,115
+ limit: u16,116
+ backwards: bool,117
+) -> Result<crate::CommandOutput, Error> {118
+ let mut options = if backwards {119
+ MessagesOptions::backward()120
+ } else {121
+ MessagesOptions::forward()122
+ };123
+ options = options.from(token);124
+ options.limit = ruma::UInt::from(limit);125
+ let messages = room126
+ .messages(options)127
+ .await128
+ .map_err(|source| Error::from_sdk("fetching event context page failed", source))?;129
+130
+ let ids: Vec<_> = messages131
+ .chunk132
+ .iter()133
+ .filter_map(|event| event.event_id())134
+ .collect();135
+ let normalized = if let Some(target) = ids.first() {136
+ let focus = TimelineFocus::Event {137
+ target: target.clone(),138
+ num_context_events: limit,139
+ thread_mode: TimelineEventFocusThreadMode::Automatic {140
+ hide_threaded_events: false,141
+ },142
+ };143
+ let timeline = room144
+ .timeline_builder()145
+ .with_focus(focus)146
+ .build()147
+ .await148
+ .map_err(|source| {149
+ Error::with_source(150
+ ErrorKind::Timeline,151
+ "building event context page failed",152
+ source,153
+ )154
+ })?;155
+ normalize_items(timeline.items().await, current_user_id.as_deref())156
+ } else {157
+ Vec::new()158
+ };159
+160
+ let mut by_id = std::collections::HashMap::new();161
+ for item in normalized {162
+ if let TimelineItem::Event(event) = &item {163
+ if let Some(event_id) = &event.event_id {164
+ by_id.insert(event_id.clone(), item);165
+ }166
+ }167
+ }168
+ let mut events: Vec<_> = ids.iter().filter_map(|id| by_id.remove(id)).collect();169
+ if backwards {170
+ events.reverse();171
+ }172
+ let next_token = messages.end.clone();173
+ let end_reached = next_token.is_none();174
+ let direction = if backwards {175
+ EventContextDirection::Before176
+ } else {177
+ EventContextDirection::After178
+ };179
+ Ok(crate::CommandOutput::EventContextPage(EventContextPage {180
+ direction,181
+ events,182
+ next_token,183
+ end_reached,184
+ }))185
+}186
+187
+fn validate_limit(limit: u16) -> Result<(), Error> {188
+ if limit > MAX_CONTEXT_LIMIT {189
+ Err(Error::new(ErrorKind::Invalid, "context limit exceeds 50"))190
+ } else {191
+ Ok(())192
+ }193
+}194
+195
+fn normalize_items(196
+ items: imbl::Vector<Arc<matrix_sdk_ui::timeline::TimelineItem>>,197
+ current_user_id: Option<&ruma::UserId>,198
+) -> Vec<TimelineItem> {199
+ let scope = MediaScope::new();200
+ normalise_timeline(items.iter(), current_user_id, &scope)201
+ .items202
+ .into_iter()203
+ .collect()204
+}205
+206
+fn split_items(207
+ items: Vec<TimelineItem>,208
+ event_id: &OwnedEventId,209
+) -> (Vec<TimelineItem>, Option<TimelineItem>, Vec<TimelineItem>) {210
+ let items: Vec<_> = items211
+ .into_iter()212
+ .filter(|item| matches!(item, TimelineItem::Event(_)))213
+ .collect();214
+ let target = items.iter().position(|item| {215
+ matches!(item, TimelineItem::Event(event) if event.event_id.as_ref() == Some(event_id))216
+ });217
+ let Some(index) = target else {218
+ return (items, None, Vec::new());219
+ };220
+ let mut before = items;221
+ let after = before.split_off(index + 1);222
+ let event = before.pop();223
+ (before, event, after)224
+}@@ -1,4 +1,5 @@
11
mod commands;2
+mod context;23
pub(crate) mod executor;34
mod media;45
mod mutations;@@ -742,6 +743,21 @@ impl Engine {
742743
},743744
),744745
Command::Timeline(command) => self.route_timeline(command, reply),746
+ Command::EventContext(command) => {747
+ let room_id = match &command {748
+ crate::EventContextCommand::Fetch { room_id, .. }749
+ | crate::EventContextCommand::Before { room_id, .. }750
+ | crate::EventContextCommand::After { room_id, .. } => room_id,751
+ };752
+ let Some(room) = client.get_room(room_id) else {753
+ reply.done(Err(Error::room_unknown()));754
+ return None;755
+ };756
+ let current_user_id = client.user_id().map(ToOwned::to_owned);757
+ self.tasks.spawn(TaskContext::Command("event context"), async move {758
+ reply.send(context::execute(room, current_user_id, command).await);759
+ });760
+ }745761
Command::Media(MediaCommand::Upload(upload)) => self.upload(upload, reply),746762
Command::Encryption(command) => enqueue(&self.encryption, Job { command, reply }),747763
Command::Notification(command) => {@@ -53,6 +53,7 @@ pub enum ErrorKind {
5353
RoomUnknown,5454
RoomNotOpen,5555
TimelineNotOpen,56
+ EventNotFound,5657
RoomBusy,5758
QueueFull,5859
EmptyBody,@@ -62,7 +62,8 @@ pub use model::{
6262
Sender, SpaceChild, Termination, ThreadSummary, TimelineCommand, TimelineContent, TimelineDiff,6363
TimelineDiffError, TimelineEvent, TimelineId, TimelineItem, TimelineItemId, TypingState,6464
UploadDetails, UploadOutcome, UploadThumbnail, UtdCause, VerificationAction,65
- VerificationFailure, VerificationState, VerificationTarget,65
+ VerificationFailure, VerificationState, VerificationTarget, EventContext, EventContextCommand,66
+ EventContextDirection, EventContextPage,6667
};6768
pub use ruma;6869
pub use transport::{ClientHandle, EventStream, Pending, SpaceHierarchy};@@ -23,6 +23,7 @@ pub enum Command {
2323
Account(AccountCommand),2424
Room(RoomCommand),2525
Timeline(TimelineCommand),26
+ EventContext(EventContextCommand),2627
Media(MediaCommand),2728
Encryption(EncryptionCommand),2829
Notification(NotificationCommand),@@ -30,6 +31,28 @@ pub enum Command {
3031
Pack(PackCommand),3132
}3233
34
+#[derive(Clone, Debug, Eq, PartialEq)]35
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]36
+#[non_exhaustive]37
+pub enum EventContextCommand {38
+ Fetch {39
+ room_id: OwnedRoomId,40
+ event_id: OwnedEventId,41
+ before_limit: u16,42
+ after_limit: u16,43
+ },44
+ Before {45
+ room_id: OwnedRoomId,46
+ limit: u16,47
+ token: String,48
+ },49
+ After {50
+ room_id: OwnedRoomId,51
+ limit: u16,52
+ token: String,53
+ },54
+}55
+3356
#[derive(Debug)]3457
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]3558
#[non_exhaustive]@@ -640,6 +663,36 @@ pub enum CommandOutput {
640663
shortcodes: Vec<String>,641664
skipped: Vec<String>,642665
},666
+ EventContext(EventContext),667
+ EventContextPage(EventContextPage),668
+}669
+670
+#[derive(Clone, Debug, Eq, PartialEq)]671
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]672
+#[non_exhaustive]673
+pub struct EventContext {674
+ pub events_before: Vec<TimelineItem>,675
+ pub event: Option<TimelineItem>,676
+ pub events_after: Vec<TimelineItem>,677
+ pub start_token: Option<String>,678
+ pub end_token: Option<String>,679
+}680
+681
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]682
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]683
+pub enum EventContextDirection {684
+ Before,685
+ After,686
+}687
+688
+#[derive(Clone, Debug, Eq, PartialEq)]689
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]690
+#[non_exhaustive]691
+pub struct EventContextPage {692
+ pub direction: EventContextDirection,693
+ pub events: Vec<TimelineItem>,694
+ pub next_token: Option<String>,695
+ pub end_reached: bool,643696
}644697
645698
#[derive(Clone, Debug, Eq, PartialEq)]@@ -28,7 +28,8 @@ use tokio::sync::{OnceCell, mpsc, oneshot, watch};
2828
2929
use crate::{3030
AccountCommand, AttachmentUpload, Command, CommandOutput, ConnectionState, CreateRoomOptions,31
- EncryptionCommand, EncryptionState, Error, ErrorKind, Event, EventRef, ImagePackSource,31
+ EncryptionCommand, EncryptionState, Error, ErrorKind, Event, EventContext, EventContextCommand,32
+ EventContextPage, EventRef, ImagePackSource,3233
ImagePacks, InteractiveAuth, InteractiveAuthOutcome, LifecycleCommand, LinkPreview, Media,3334
MediaCommand, MediaId, MediaLocation, MediaSize, MemberFilter, MemberSummary, MessageContent,3435
MessageEditContent, NotificationCommand, OwnDevice, PackCommand, PackImageInfo, PackUsage,@@ -325,6 +326,65 @@ impl ClientHandle {
325326
.await326327
}327328
329
+ pub async fn event_context(330
+ &self,331
+ room_id: &RoomId,332
+ event_id: &EventId,333
+ before_limit: u32,334
+ after_limit: u32,335
+ ) -> Result<EventContext, Error> {336
+ match self337
+ .execute(Command::EventContext(EventContextCommand::Fetch {338
+ room_id: room_id.to_owned(),339
+ event_id: event_id.to_owned(),340
+ before_limit: before_limit.min(50) as u16,341
+ after_limit: after_limit.min(50) as u16,342
+ }))343
+ .await?344
+ {345
+ CommandOutput::EventContext(context) => Ok(context),346
+ output => Err(unexpected("event context", &output)),347
+ }348
+ }349
+350
+ pub async fn event_context_before(351
+ &self,352
+ room_id: &RoomId,353
+ token: &str,354
+ limit: u32,355
+ ) -> Result<EventContextPage, Error> {356
+ self.event_context_page(EventContextCommand::Before {357
+ room_id: room_id.to_owned(),358
+ limit: limit.min(50) as u16,359
+ token: token.to_owned(),360
+ })361
+ .await362
+ }363
+364
+ pub async fn event_context_after(365
+ &self,366
+ room_id: &RoomId,367
+ token: &str,368
+ limit: u32,369
+ ) -> Result<EventContextPage, Error> {370
+ self.event_context_page(EventContextCommand::After {371
+ room_id: room_id.to_owned(),372
+ limit: limit.min(50) as u16,373
+ token: token.to_owned(),374
+ })375
+ .await376
+ }377
+378
+ async fn event_context_page(379
+ &self,380
+ command: EventContextCommand,381
+ ) -> Result<EventContextPage, Error> {382
+ match self.execute(Command::EventContext(command)).await? {383
+ CommandOutput::EventContextPage(page) => Ok(page),384
+ output => Err(unexpected("event context page", &output)),385
+ }386
+ }387
+328388
pub async fn send_message(329389
&self,330390
timeline_id: TimelineId,