Skip to content

Commit dddc607

Browse files
authored
timeline: add event focus mode for permalinks
timeline: add event focus mode for permalinks
2 parents 4156170 + 25f893b commit dddc607

File tree

18 files changed

+837
-127
lines changed

18 files changed

+837
-127
lines changed

bindings/matrix-sdk-ffi/src/room.rs

+48-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ use std::sync::Arc;
22

33
use anyhow::{Context, Result};
44
use matrix_sdk::{
5+
event_cache::paginator::PaginatorError,
56
room::{power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole},
67
RoomMemberships, RoomState,
78
};
8-
use matrix_sdk_ui::timeline::RoomExt;
9+
use matrix_sdk_ui::timeline::{PaginationError, RoomExt, TimelineFocus};
910
use mime::Mime;
1011
use ruma::{
1112
api::client::room::report_content,
@@ -30,7 +31,7 @@ use crate::{
3031
room_info::RoomInfo,
3132
room_member::RoomMember,
3233
ruma::ImageInfo,
33-
timeline::{EventTimelineItem, ReceiptType, Timeline},
34+
timeline::{EventTimelineItem, FocusEventError, ReceiptType, Timeline},
3435
utils::u64_to_uint,
3536
TaskHandle,
3637
};
@@ -167,6 +168,48 @@ impl Room {
167168
}
168169
}
169170

171+
/// Returns a timeline focused on the given event.
172+
///
173+
/// Note: this timeline is independent from that returned with
174+
/// [`Self::timeline`], and as such it is not cached.
175+
pub async fn timeline_focused_on_event(
176+
&self,
177+
event_id: String,
178+
num_context_events: u16,
179+
internal_id_prefix: Option<String>,
180+
) -> Result<Arc<Timeline>, FocusEventError> {
181+
let parsed_event_id = EventId::parse(&event_id).map_err(|err| {
182+
FocusEventError::InvalidEventId { event_id: event_id.clone(), err: err.to_string() }
183+
})?;
184+
185+
let room = &self.inner;
186+
187+
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
188+
189+
if let Some(internal_id_prefix) = internal_id_prefix {
190+
builder = builder.with_internal_id_prefix(internal_id_prefix);
191+
}
192+
193+
let timeline = match builder
194+
.with_focus(TimelineFocus::Event { target: parsed_event_id, num_context_events })
195+
.build()
196+
.await
197+
{
198+
Ok(t) => t,
199+
Err(err) => {
200+
if let matrix_sdk_ui::timeline::Error::PaginationError(
201+
PaginationError::Paginator(PaginatorError::EventNotFound(..)),
202+
) = err
203+
{
204+
return Err(FocusEventError::EventNotFound { event_id: event_id.to_string() });
205+
}
206+
return Err(FocusEventError::Other { msg: err.to_string() });
207+
}
208+
};
209+
210+
Ok(Timeline::new(timeline))
211+
}
212+
170213
pub fn display_name(&self) -> Result<String, ClientError> {
171214
let r = self.inner.clone();
172215
RUNTIME.block_on(async move { Ok(r.display_name().await?.to_string()) })
@@ -237,7 +280,8 @@ impl Room {
237280
}
238281
}
239282

240-
// Otherwise, fallback to the classical path.
283+
// Otherwise, create a synthetic [`EventTimelineItem`] using the classical
284+
// [`Room`] path.
241285
let latest_event = match self.inner.latest_event() {
242286
Some(latest_event) => matrix_sdk_ui::timeline::EventTimelineItem::from_latest_event(
243287
self.inner.client(),
@@ -249,6 +293,7 @@ impl Room {
249293
.map(Arc::new),
250294
None => None,
251295
};
296+
252297
Ok(RoomInfo::new(&self.inner, avatar_url, latest_event).await?)
253298
}
254299

bindings/matrix-sdk-ffi/src/timeline/mod.rs

+27-8
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use matrix_sdk::attachment::{
2222
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
2323
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
2424
};
25-
use matrix_sdk_ui::timeline::{BackPaginationStatus, EventItemOrigin, Profile, TimelineDetails};
25+
use matrix_sdk_ui::timeline::{EventItemOrigin, PaginationStatus, Profile, TimelineDetails};
2626
use mime::Mime;
2727
use ruma::{
2828
events::{
@@ -162,7 +162,7 @@ impl Timeline {
162162

163163
pub fn subscribe_to_back_pagination_status(
164164
&self,
165-
listener: Box<dyn BackPaginationStatusListener>,
165+
listener: Box<dyn PaginationStatusListener>,
166166
) -> Result<Arc<TaskHandle>, ClientError> {
167167
let mut subscriber = self.inner.back_pagination_status();
168168

@@ -176,11 +176,18 @@ impl Timeline {
176176
}))))
177177
}
178178

179-
/// Loads older messages into the timeline.
179+
/// Paginate backwards, whether we are in focused mode or in live mode.
180180
///
181-
/// Raises an exception if there are no timeline listeners.
182-
pub fn paginate_backwards(&self, opts: PaginationOptions) -> Result<(), ClientError> {
183-
RUNTIME.block_on(async { Ok(self.inner.paginate_backwards(opts.into()).await?) })
181+
/// Returns whether we hit the end of the timeline or not.
182+
pub async fn paginate_backwards(&self, num_events: u16) -> Result<bool, ClientError> {
183+
Ok(self.inner.paginate_backwards(num_events).await?)
184+
}
185+
186+
/// Paginate forwards, when in focused mode.
187+
///
188+
/// Returns whether we hit the end of the timeline or not.
189+
pub async fn focused_paginate_forwards(&self, num_events: u16) -> Result<bool, ClientError> {
190+
Ok(self.inner.focused_paginate_forwards(num_events).await?)
184191
}
185192

186193
pub fn send_read_receipt(
@@ -573,6 +580,18 @@ impl Timeline {
573580
}
574581
}
575582

583+
#[derive(Debug, thiserror::Error, uniffi::Error)]
584+
pub enum FocusEventError {
585+
#[error("the event id parameter {event_id} is incorrect: {err}")]
586+
InvalidEventId { event_id: String, err: String },
587+
588+
#[error("the event {event_id} could not be found")]
589+
EventNotFound { event_id: String },
590+
591+
#[error("error when trying to focus on an event: {msg}")]
592+
Other { msg: String },
593+
}
594+
576595
#[derive(uniffi::Record)]
577596
pub struct RoomTimelineListenerResult {
578597
pub items: Vec<Arc<TimelineItem>>,
@@ -585,8 +604,8 @@ pub trait TimelineListener: Sync + Send {
585604
}
586605

587606
#[uniffi::export(callback_interface)]
588-
pub trait BackPaginationStatusListener: Sync + Send {
589-
fn on_update(&self, status: BackPaginationStatus);
607+
pub trait PaginationStatusListener: Sync + Send {
608+
fn on_update(&self, status: PaginationStatus);
590609
}
591610

592611
#[derive(Clone, uniffi::Object)]

crates/matrix-sdk-ui/src/room_list_service/mod.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ use tokio::{
9595
time::timeout,
9696
};
9797

98+
use crate::timeline;
99+
98100
/// The [`RoomListService`] type. See the module's documentation to learn more.
99101
#[derive(Debug)]
100102
pub struct RoomListService {
@@ -553,7 +555,7 @@ pub enum Error {
553555
TimelineAlreadyExists(OwnedRoomId),
554556

555557
#[error("An error occurred while initializing the timeline")]
556-
InitializingTimeline(#[source] EventCacheError),
558+
InitializingTimeline(#[source] timeline::Error),
557559

558560
#[error("The attached event cache ran into an error")]
559561
EventCache(#[from] EventCacheError),

crates/matrix-sdk-ui/src/timeline/builder.rs

+38-24
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ use std::{collections::BTreeSet, sync::Arc};
1616

1717
use eyeball::SharedObservable;
1818
use futures_util::{pin_mut, StreamExt};
19-
use matrix_sdk::{
20-
event_cache::{self, RoomEventCacheUpdate},
21-
executor::spawn,
22-
Room,
23-
};
19+
use matrix_sdk::{event_cache::RoomEventCacheUpdate, executor::spawn, Room};
2420
use ruma::{events::AnySyncTimelineEvent, RoomVersionId};
2521
use tokio::sync::{broadcast, mpsc};
2622
use tracing::{info, info_span, trace, warn, Instrument, Span};
@@ -30,9 +26,12 @@ use super::to_device::{handle_forwarded_room_key_event, handle_room_key_event};
3026
use super::{
3127
inner::{TimelineInner, TimelineInnerSettings},
3228
queue::send_queued_messages,
33-
BackPaginationStatus, Timeline, TimelineDropHandle,
29+
Error, Timeline, TimelineDropHandle, TimelineFocus,
30+
};
31+
use crate::{
32+
timeline::{event_item::RemoteEventOrigin, PaginationStatus},
33+
unable_to_decrypt_hook::UtdHookManager,
3434
};
35-
use crate::unable_to_decrypt_hook::UtdHookManager;
3635

3736
/// Builder that allows creating and configuring various parts of a
3837
/// [`Timeline`].
@@ -41,6 +40,7 @@ use crate::unable_to_decrypt_hook::UtdHookManager;
4140
pub struct TimelineBuilder {
4241
room: Room,
4342
settings: TimelineInnerSettings,
43+
focus: TimelineFocus,
4444

4545
/// An optional hook to call whenever we run into an unable-to-decrypt or a
4646
/// late-decryption event.
@@ -56,10 +56,19 @@ impl TimelineBuilder {
5656
room: room.clone(),
5757
settings: TimelineInnerSettings::default(),
5858
unable_to_decrypt_hook: None,
59+
focus: TimelineFocus::Live,
5960
internal_id_prefix: None,
6061
}
6162
}
6263

64+
/// Sets up the initial focus for this timeline.
65+
///
66+
/// This can be changed later on while the timeline is alive.
67+
pub fn with_focus(mut self, focus: TimelineFocus) -> Self {
68+
self.focus = focus;
69+
self
70+
}
71+
6372
/// Sets up a hook to catch unable-to-decrypt (UTD) events for the timeline
6473
/// we're building.
6574
///
@@ -134,8 +143,8 @@ impl TimelineBuilder {
134143
track_read_receipts = self.settings.track_read_receipts,
135144
)
136145
)]
137-
pub async fn build(self) -> event_cache::Result<Timeline> {
138-
let Self { room, settings, unable_to_decrypt_hook, internal_id_prefix } = self;
146+
pub async fn build(self) -> Result<Timeline, Error> {
147+
let Self { room, settings, unable_to_decrypt_hook, focus, internal_id_prefix } = self;
139148

140149
let client = room.client();
141150
let event_cache = client.event_cache();
@@ -144,14 +153,12 @@ impl TimelineBuilder {
144153
event_cache.subscribe()?;
145154

146155
let (room_event_cache, event_cache_drop) = room.event_cache().await?;
147-
let (events, mut event_subscriber) = room_event_cache.subscribe().await?;
148-
149-
let has_events = !events.is_empty();
156+
let (_, mut event_subscriber) = room_event_cache.subscribe().await?;
150157

151-
let inner = TimelineInner::new(room, internal_id_prefix, unable_to_decrypt_hook)
158+
let inner = TimelineInner::new(room, focus, internal_id_prefix, unable_to_decrypt_hook)
152159
.with_settings(settings);
153160

154-
inner.replace_with_initial_events(events).await;
161+
let has_events = inner.init_focus(&room_event_cache).await?;
155162

156163
let room = inner.room();
157164
let client = room.client();
@@ -165,10 +172,10 @@ impl TimelineBuilder {
165172
span.follows_from(Span::current());
166173

167174
async move {
168-
trace!("Spawned the event subscriber task");
175+
trace!("Spawned the event subscriber task.");
169176

170177
loop {
171-
trace!("Waiting for an event");
178+
trace!("Waiting for an event.");
172179

173180
let update = match event_subscriber.recv().await {
174181
Ok(up) => up,
@@ -187,7 +194,7 @@ impl TimelineBuilder {
187194
// current timeline.
188195
match room_event_cache.subscribe().await {
189196
Ok((events, _)) => {
190-
inner.replace_with_initial_events(events).await;
197+
inner.replace_with_initial_events(events, RemoteEventOrigin::Sync).await;
191198
}
192199
Err(err) => {
193200
warn!("Error when re-inserting initial events into the timeline: {err}");
@@ -200,18 +207,25 @@ impl TimelineBuilder {
200207
};
201208

202209
match update {
203-
RoomEventCacheUpdate::Clear => {
204-
trace!("Clearing the timeline.");
205-
inner.clear().await;
206-
}
207-
208210
RoomEventCacheUpdate::UpdateReadMarker { event_id } => {
209211
trace!(target = %event_id, "Handling fully read marker.");
210212
inner.handle_fully_read_marker(event_id).await;
211213
}
212214

215+
RoomEventCacheUpdate::Clear => {
216+
if !inner.is_live().await {
217+
// Ignore a clear for a timeline not in the live mode; the
218+
// focused-on-event mode doesn't add any new items to the timeline
219+
// anyways.
220+
continue;
221+
}
222+
223+
trace!("Clearing the timeline.");
224+
inner.clear().await;
225+
}
226+
213227
RoomEventCacheUpdate::Append { events, ephemeral, ambiguity_changes } => {
214-
trace!("Received new events");
228+
trace!("Received new events from sync.");
215229

216230
// TODO: (bnjbvr) ephemeral should be handled by the event cache, and
217231
// we should replace this with a simple `add_events_at`.
@@ -300,7 +314,7 @@ impl TimelineBuilder {
300314

301315
let timeline = Timeline {
302316
inner,
303-
back_pagination_status: SharedObservable::new(BackPaginationStatus::Idle),
317+
back_pagination_status: SharedObservable::new(PaginationStatus::Idle),
304318
msg_sender,
305319
event_cache: room_event_cache,
306320
drop_handle: Arc::new(TimelineDropHandle {

crates/matrix-sdk-ui/src/timeline/error.rs

+26-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
use std::fmt;
1616

17+
use matrix_sdk::event_cache::{paginator::PaginatorError, EventCacheError};
1718
use thiserror::Error;
1819

1920
/// Errors specific to the timeline.
@@ -28,33 +29,52 @@ pub enum Error {
2829
#[error("Event not found, can't retry sending")]
2930
RetryEventNotInTimeline,
3031

31-
/// The event is currently unsupported for this use case.
32+
/// The event is currently unsupported for this use case..
3233
#[error("Unsupported event")]
3334
UnsupportedEvent,
3435

35-
/// Couldn't read the attachment data from the given URL
36+
/// Couldn't read the attachment data from the given URL.
3637
#[error("Invalid attachment data")]
3738
InvalidAttachmentData,
3839

39-
/// The attachment file name used as a body is invalid
40+
/// The attachment file name used as a body is invalid.
4041
#[error("Invalid attachment file name")]
4142
InvalidAttachmentFileName,
4243

43-
/// The attachment could not be sent
44+
/// The attachment could not be sent.
4445
#[error("Failed sending attachment")]
4546
FailedSendingAttachment,
4647

47-
/// The reaction could not be toggled
48+
/// The reaction could not be toggled.
4849
#[error("Failed toggling reaction")]
4950
FailedToToggleReaction,
5051

5152
/// The room is not in a joined state.
5253
#[error("Room is not joined")]
5354
RoomNotJoined,
5455

55-
/// Could not get user
56+
/// Could not get user.
5657
#[error("User ID is not available")]
5758
UserIdNotAvailable,
59+
60+
/// Something went wrong with the room event cache.
61+
#[error("Something went wrong with the room event cache.")]
62+
EventCacheError(#[from] EventCacheError),
63+
64+
/// An error happened during pagination.
65+
#[error("An error happened during pagination.")]
66+
PaginationError(#[from] PaginationError),
67+
}
68+
69+
#[derive(Error, Debug)]
70+
pub enum PaginationError {
71+
/// The timeline isn't in the event focus mode.
72+
#[error("The timeline isn't in the event focus mode")]
73+
NotEventFocusMode,
74+
75+
/// An error occurred while paginating.
76+
#[error("Error when paginating.")]
77+
Paginator(#[source] PaginatorError),
5878
}
5979

6080
#[derive(Error)]

0 commit comments

Comments
 (0)