Skip to content
4 changes: 2 additions & 2 deletions bindings/matrix-sdk-ffi/src/timeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,9 @@ impl Timeline {
Ok(())
}

/// Returns the [`EventId`] of the latest event in the timeline.
/// Returns the latest [`EventId`] in the timeline.
pub async fn latest_event_id(&self) -> Option<String> {
self.inner.latest_event().await.and_then(|event| event.event_id().map(ToString::to_string))
self.inner.latest_event_id().await.as_deref().map(ToString::to_string)
}

/// Queues an event in the room's send queue so it's processed for
Expand Down
10 changes: 8 additions & 2 deletions crates/matrix-sdk-ui/src/timeline/controller/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,10 +577,16 @@ pub(in crate::timeline) struct EventMeta {
/// Note that the #2 timeline item (the day divider) doesn't map to any
/// remote event, but if it moves, it has an impact on this mapping.
pub timeline_item_index: Option<usize>,

pub thread_root_id: Option<OwnedEventId>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add documentation for this field. Also —just a personal taste— I would put this field after event_id, not mandatory.

}

impl EventMeta {
pub fn new(event_id: OwnedEventId, visible: bool) -> Self {
Self { event_id, visible, timeline_item_index: None }
pub fn new(
event_id: OwnedEventId,
visible: bool,
thread_root_id: Option<OwnedEventId>,
) -> Self {
Self { event_id, visible, timeline_item_index: None, thread_root_id }
}
}
22 changes: 21 additions & 1 deletion crates/matrix-sdk-ui/src/timeline/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1761,7 +1761,27 @@ impl TimelineController {
/// it's folded into another timeline item.
pub(crate) async fn latest_event_id(&self) -> Option<OwnedEventId> {
let state = self.state.read().await;
state.items.all_remote_events().last().map(|event_meta| &event_meta.event_id).cloned()
let filter_out_thread_events = match self.focus() {
TimelineFocusKind::Thread { .. } => false,
TimelineFocusKind::Live { hide_threaded_events } => hide_threaded_events.to_owned(),
TimelineFocusKind::Event { paginator } => {
paginator.get().is_some_and(|paginator| paginator.hide_threaded_events())
}
_ => true,
};
state
.items
.all_remote_events()
.iter()
.rev()
.filter_map(|item| {
if !filter_out_thread_events || item.thread_root_id.is_none() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain with an inline comment here why we want to filter out event with a thread root?

Some(item.event_id.clone())
} else {
None
}
})
.next()
}

#[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,12 @@ mod observable_items_tests {
}

fn event_meta(event_id: &str) -> EventMeta {
EventMeta { event_id: event_id.parse().unwrap(), timeline_item_index: None, visible: false }
EventMeta {
event_id: event_id.parse().unwrap(),
timeline_item_index: None,
visible: false,
thread_root_id: None,
}
}

macro_rules! assert_event_id {
Expand Down Expand Up @@ -1923,6 +1928,7 @@ impl AllRemoteEvents {
}

/// Return a reference to the last remote event if it exists.
#[cfg(test)]
pub fn last(&self) -> Option<&EventMeta> {
self.0.back()
}
Expand Down Expand Up @@ -2054,7 +2060,12 @@ mod all_remote_events_tests {
use super::{AllRemoteEvents, EventMeta};

fn event_meta(event_id: &str, timeline_item_index: Option<usize>) -> EventMeta {
EventMeta { event_id: event_id.parse().unwrap(), timeline_item_index, visible: false }
EventMeta {
event_id: event_id.parse().unwrap(),
timeline_item_index,
visible: false,
thread_root_id: None,
}
}

macro_rules! assert_events {
Expand Down
100 changes: 52 additions & 48 deletions crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
MilliSecondsSinceUnixEpoch,
Option<OwnedTransactionId>,
Option<TimelineAction>,
Option<OwnedEventId>,
bool,
)> {
let state_key: Option<String> = raw.get_field("state_key").ok().flatten();
Expand Down Expand Up @@ -534,6 +535,7 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
origin_server_ts,
transaction_id,
Some(TimelineAction::failed_to_parse(event_type, deserialization_error)),
None,
true,
))
}
Expand All @@ -550,7 +552,7 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
// Remember the event before returning prematurely.
// See [`ObservableItems::all_remote_events`].
self.add_or_update_remote_event(
EventMeta::new(event_id, false),
EventMeta::new(event_id, false, None),
sender.as_deref(),
origin_server_ts,
position,
Expand Down Expand Up @@ -628,63 +630,65 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
_ => (event.kind.into_raw(), None),
};

let (event_id, sender, timestamp, txn_id, timeline_action, should_add) = match raw
.deserialize()
{
// Classical path: the event is valid, can be deserialized, everything is alright.
Ok(event) => {
let (in_reply_to, thread_root) = self.meta.process_event_relations(
&event,
&raw,
bundled_edit_encryption_info,
&self.items,
self.focus.is_thread(),
);

let should_add = self.should_add_event_item(
room_data_provider,
settings,
&event,
thread_root.as_deref(),
position,
);

(
event.event_id().to_owned(),
event.sender().to_owned(),
event.origin_server_ts(),
event.transaction_id().map(ToOwned::to_owned),
TimelineAction::from_event(
event,
let (event_id, sender, timestamp, txn_id, timeline_action, thread_root, should_add) =
match raw.deserialize() {
// Classical path: the event is valid, can be deserialized, everything is alright.
Ok(event) => {
let (in_reply_to, thread_root) = self.meta.process_event_relations(
&event,
&raw,
bundled_edit_encryption_info,
&self.items,
self.focus.is_thread(),
);

let should_add = self.should_add_event_item(
room_data_provider,
utd_info
.map(|utd_info| (utd_info, self.meta.unable_to_decrypt_hook.as_ref())),
in_reply_to,
settings,
&event,
thread_root.as_deref(),
position,
);

(
event.event_id().to_owned(),
event.sender().to_owned(),
event.origin_server_ts(),
event.transaction_id().map(ToOwned::to_owned),
TimelineAction::from_event(
event,
&raw,
room_data_provider,
utd_info.map(|utd_info| {
(utd_info, self.meta.unable_to_decrypt_hook.as_ref())
}),
in_reply_to,
thread_root.clone(),
thread_summary,
)
.await,
thread_root,
thread_summary,
should_add,
)
.await,
should_add,
)
}
}

// The event seems invalid…
Err(e) => {
if let Some(tuple) =
self.maybe_add_error_item(position, room_data_provider, &raw, e, settings).await
{
tuple
} else {
return false;
// The event seems invalid…
Err(e) => {
if let Some(tuple) = self
.maybe_add_error_item(position, room_data_provider, &raw, e, settings)
.await
{
tuple
} else {
return false;
}
}
}
};
};

// Remember the event.
// See [`ObservableItems::all_remote_events`].
self.add_or_update_remote_event(
EventMeta::new(event_id.clone(), should_add),
EventMeta::new(event_id.clone(), should_add, thread_root),
Some(&sender),
Some(timestamp),
position,
Expand Down
7 changes: 6 additions & 1 deletion crates/matrix-sdk-ui/src/timeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ impl Timeline {
Some(item.to_owned())
}

/// Get the latest of the timeline's event items.
/// Get the latest of the timeline's event items, both remote and local.
pub async fn latest_event(&self) -> Option<EventTimelineItem> {
if self.controller.is_live() {
self.controller.items().await.iter().rev().find_map(|item| {
Expand All @@ -268,6 +268,11 @@ impl Timeline {
}
}

/// Get the latest of the timeline's event ids.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to precise that, contrary to latest_event, this method only returns the ID of the last remote event (which is obvious for us because a local event doesn't have an event ID, only a transaction ID, but it's better to make it clear.

pub async fn latest_event_id(&self) -> Option<OwnedEventId> {
self.controller.latest_event_id().await
}

/// Get the current timeline items, along with a stream of updates of
/// timeline items.
///
Expand Down
97 changes: 92 additions & 5 deletions crates/matrix-sdk-ui/src/timeline/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use imbl::vector;
use matrix_sdk::{assert_next_with_timeout, test_utils::mocks::MatrixMockServer};
use matrix_sdk_base::ThreadingSupport;
use matrix_sdk_test::{
ALICE, BOB, CAROL, async_test,
ALICE, BOB, CAROL, JoinedRoomBuilder, async_test,
event_factory::{EventFactory, PreviousMembership},
};
use ruma::{
Expand All @@ -33,14 +35,14 @@ use ruma::{
topic::RedactedRoomTopicEventContent,
},
},
mxc_uri, owned_event_id, owned_mxc_uri, user_id,
mxc_uri, owned_event_id, owned_mxc_uri, room_id, user_id,
};
use stream_assert::assert_next_matches;
use stream_assert::{assert_next_matches, assert_pending};

use super::TestTimeline;
use crate::timeline::{
MembershipChange, MsgLikeContent, MsgLikeKind, TimelineDetails, TimelineItemContent,
TimelineItemKind, VirtualTimelineItem,
MembershipChange, MsgLikeContent, MsgLikeKind, RoomExt, TimelineDetails, TimelineFocus,
TimelineItemContent, TimelineItemKind, VirtualTimelineItem,
controller::TimelineSettings,
event_item::{AnyOtherFullStateEventContent, RemoteEventOrigin},
tests::{ReadReceiptMap, TestRoomDataProvider, TestTimelineBuilder},
Expand Down Expand Up @@ -500,3 +502,88 @@ async fn test_replace_with_initial_events_when_batched() {
assert_matches!(value.as_virtual(), Some(VirtualTimelineItem::DateDivider(_)));
});
}

#[async_test]
async fn test_latest_event_id_in_main_timeline() {
let server = MatrixMockServer::new().await;
let client = server
.client_builder()
.on_builder(|b| {
b.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
})
.build()
.await;

let room_id = room_id!("!a98sd12bjh:example.org");
let event_id = event_id!("$message");
let reaction_event_id = event_id!("$reaction");
let thread_event_id = event_id!("$thread");

let room = server.sync_joined_room(&client, room_id).await;
let timeline = room
.timeline_builder()
.with_focus(TimelineFocus::Live { hide_threaded_events: true })
.track_read_marker_and_receipts()
.build()
.await
.expect("Could not build live timeline");

let (items, mut stream) = timeline.subscribe().await;
assert!(items.is_empty());
assert_pending!(stream);

let f = EventFactory::new().room(room_id).sender(user_id!("@a:b.c"));

// If we receive a message in the live timeline
server
.sync_room(
&client,
JoinedRoomBuilder::new(room_id)
.add_timeline_event(f.text_msg("A message").event_id(event_id).into_raw_sync()),
)
.await;

let items = assert_next_with_timeout!(stream);
// We receive the day divider and the items
assert_eq!(items.len(), 2);
assert_let!(Some(latest_event_id) = timeline.controller.latest_event_id().await);
// The latest event id is from the event in the live timeline
assert_eq!(event_id, latest_event_id);

// If we then receive a reaction
server
.sync_room(
&client,
JoinedRoomBuilder::new(room_id).add_timeline_event(
f.reaction(event_id, ":D").event_id(reaction_event_id).into_raw_sync(),
),
)
.await;

let items = assert_next_with_timeout!(stream);
// The reaction is added
assert_eq!(items.len(), 1);
assert_let!(Some(latest_event_id) = timeline.controller.latest_event_id().await);
// And it's now the latest event id
assert_eq!(reaction_event_id, latest_event_id);

// If we now get a message in a thread inside that room
server
.sync_room(
&client,
JoinedRoomBuilder::new(room_id).add_timeline_event(
f.text_msg("In thread")
.in_thread(event_id, thread_event_id)
.event_id(thread_event_id)
.into_raw_sync(),
),
)
.await;
// The thread root event is updated
assert_eq!(items.len(), 1);
assert_let!(Some(latest_event_id) = timeline.controller.latest_event_id().await);

// But the latest event in the live timeline is still the reaction, since the
// threaded event is not part of the live timeline
assert_eq!(reaction_event_id, latest_event_id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2993,6 +2993,8 @@ async fn test_thread_subscriptions_extension_enabled_only_if_server_advertises_i
},
},
};

mock_server.reset().await;
}

// Then, advertise support with support for MSC4306; the extension will be
Expand All @@ -3003,7 +3005,7 @@ async fn test_thread_subscriptions_extension_enabled_only_if_server_advertises_i
.mock_versions()
.ok_custom(&["v1.11"], &features_map)
.named("/versions, second time")
.mock_once()
.up_to_n_times(2)
.mount()
.await;

Expand Down
Loading