Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bindings/matrix-sdk-ffi/changelog.d/6569.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `RoomInfo::fully_read_event_id` to expose the user's `m.fully_read` event ID.
3 changes: 3 additions & 0 deletions bindings/matrix-sdk-ffi/src/room/room_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ pub struct RoomInfo {
/// Events causing mentions/highlights for the user, according to their
/// notification settings.
num_unread_mentions: u64,
/// Event ID of the user's `m.fully_read` marker for this room, if any.
fully_read_event_id: Option<String>,
Comment thread
stefanceriu marked this conversation as resolved.
/// The currently pinned event ids.
pinned_event_ids: Vec<String>,
/// The join rule for this room, if known.
Expand Down Expand Up @@ -206,6 +208,7 @@ impl RoomInfo {
num_unread_messages: room.num_unread_messages(),
num_unread_notifications: room.num_unread_notifications(),
num_unread_mentions: room.num_unread_mentions(),
fully_read_event_id: room.fully_read_event_id().map(|id| id.to_string()),
pinned_event_ids,
join_rule,
history_visibility: room.history_visibility_or_default().try_into()?,
Expand Down
1 change: 1 addition & 0 deletions crates/matrix-sdk-base/changelog.d/6569.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `RoomInfo::fully_read_event_id` and `Room::fully_read_event_id` to expose the user's `m.fully_read` event ID.
1 change: 1 addition & 0 deletions crates/matrix-sdk-base/changelog.d/6569.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[**breaking**] `RoomInfoNotableUpdateReasons` is now a `u16` to include a `FULLY_READ` flag to notify on changes of the `m.fully_read` marker.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

use ruma::{
RoomId,
events::{AnyRoomAccountDataEvent, marked_unread::MarkedUnreadEventContent},
events::{
AnyRoomAccountDataEvent, fully_read::FullyReadEventContent,
marked_unread::MarkedUnreadEventContent,
},
serde::Raw,
};
use tracing::{instrument, warn};
Expand Down Expand Up @@ -85,6 +88,21 @@ pub fn for_room(
},
);
}
AnyRoomAccountDataEvent::FullyRead(event) => {
on_room_info(
room_id,
&mut context.state_changes,
state_store,
|room_info| {
on_fully_read_marker(
room_id,
&event.content,
room_info,
&mut context.room_info_notable_updates,
);
},
);
}

// Nothing.
_ => {}
Expand Down Expand Up @@ -128,6 +146,25 @@ fn on_room_info<F>(
}
}

// Helper to update the fully-read marker event id on the `RoomInfo` and
// notify subscribers when the value changes.
fn on_fully_read_marker(
room_id: &RoomId,
content: &FullyReadEventContent,
room_info: &mut RoomInfo,
room_info_notable_updates: &mut RoomInfoNotableUpdates,
) {
if room_info.base_info.fully_read_event_id.as_ref() == Some(&content.event_id) {
return;
}

room_info.base_info.fully_read_event_id = Some(content.event_id.clone());
room_info_notable_updates
.entry(room_id.to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::FULLY_READ);
}

// Helper to update the unread marker for stable and unstable prefixes.
fn on_unread_marker(
room_id: &RoomId,
Expand Down
6 changes: 6 additions & 0 deletions crates/matrix-sdk-base/src/room/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,12 @@ impl Room {
self.info.read().base_info.is_marked_unread
}

/// Returns the event ID of the user's `m.fully_read` marker for this room,
/// if any.
pub fn fully_read_event_id(&self) -> Option<OwnedEventId> {
self.info.read().fully_read_event_id().map(ToOwned::to_owned)
}

/// Returns the [`RoomVersionId`] of the room, if known.
pub fn version(&self) -> Option<RoomVersionId> {
self.info.read().room_version().cloned()
Expand Down
31 changes: 22 additions & 9 deletions crates/matrix-sdk-base/src/room/room_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ pub struct BaseRoomInfo {
/// others, and this field collects them.
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
pub(crate) notable_tags: RoomNotableTags,
/// The event ID of the user's `m.fully_read` marker for this room, if any.
#[serde(skip_serializing_if = "Option::is_none", default)]
pub(crate) fully_read_event_id: Option<OwnedEventId>,
/// The `m.room.pinned_events` of this room.
pub(crate) pinned_events: Option<PossiblyRedactedRoomPinnedEventsEventContent>,
}
Expand Down Expand Up @@ -557,6 +560,7 @@ impl Default for BaseRoomInfo {
is_marked_unread: false,
is_marked_unread_source: AccountDataSource::Unstable,
notable_tags: RoomNotableTags::empty(),
fully_read_event_id: None,
pinned_events: None,
}
}
Expand Down Expand Up @@ -1225,6 +1229,12 @@ impl RoomInfo {
self.base_info.pinned_events.clone().and_then(|c| c.pinned)
}

/// Returns the event ID of the user's `m.fully_read` marker for this room,
/// if any.
pub fn fully_read_event_id(&self) -> Option<&EventId> {
self.base_info.fully_read_event_id.as_deref()
}

/// Checks if an `EventId` is currently pinned.
/// It avoids having to clone the whole list of event ids to check a single
/// value.
Expand Down Expand Up @@ -1412,27 +1422,27 @@ pub struct RoomInfoNotableUpdate {
bitflags! {
/// The reason why a [`RoomInfoNotableUpdate`] is emitted.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RoomInfoNotableUpdateReasons: u8 {
pub struct RoomInfoNotableUpdateReasons: u16 {
/// The recency stamp of the `Room` has changed.
const RECENCY_STAMP = 0b0000_0001;
const RECENCY_STAMP = 0b0000_0000_0000_0001;

/// The latest event of the `Room` has changed.
const LATEST_EVENT = 0b0000_0010;
const LATEST_EVENT = 0b0000_0000_0000_0010;

/// A read receipt has changed.
const READ_RECEIPT = 0b0000_0100;
const READ_RECEIPT = 0b0000_0000_0000_0100;

/// The user-controlled unread marker value has changed.
const UNREAD_MARKER = 0b0000_1000;
const UNREAD_MARKER = 0b0000_0000_0000_1000;

/// A membership change happened for the current user.
const MEMBERSHIP = 0b0001_0000;
const MEMBERSHIP = 0b0000_0000_0001_0000;

/// The display name has changed.
const DISPLAY_NAME = 0b0010_0000;
const DISPLAY_NAME = 0b0000_0000_0010_0000;

/// The active service members have changed.
const ACTIVE_SERVICE_MEMBERS = 0b0100_0000;
const ACTIVE_SERVICE_MEMBERS = 0b0000_0000_0100_0000;

/// This is a temporary hack.
///
Expand All @@ -1444,7 +1454,10 @@ bitflags! {
/// identified, we are likely to miss particular updates, and it can feel broken.
/// Ultimately, we want to clearly identify all the notable update reasons, and
/// remove this one.
const NONE = 0b1000_0000;
const NONE = 0b0000_0000_1000_0000;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could be a good idea to use 0b1000_0000_0000_0000 for NONE, but not mandatory. Do it if you find the motivation, but not necessary here.


/// The user's `m.fully_read` marker has changed.
const FULLY_READ = 0b0000_0001_0000_0000;
}
}

Expand Down
123 changes: 123 additions & 0 deletions crates/matrix-sdk-base/src/sliding_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,129 @@ mod tests {
assert!(room_info_notable_update_stream.is_empty());
}

#[async_test]
async fn test_fully_read_marker_can_trigger_a_notable_update_reason() {
// Given a logged-in client,
let client = logged_in_base_client(None).await;
let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();

// When I receive a sliding sync response containing a new room,
let room_id = room_id!("!r:e.uk");
let room = http::response::Room::new();
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");

// Other notable updates are received, but not the ones we are interested by.
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
}
);
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
}
);
assert!(room_info_notable_update_stream.is_empty());

// When I receive a sliding sync response containing an `m.fully_read`
// account-data event,
let room_account_data_events = vec![
Raw::from_json_string(
json!({
"type": "m.fully_read",
"content": { "event_id": "$first" },
})
.to_string(),
)
.unwrap(),
];
let mut response = response_with_room(room_id, http::response::Room::new());
response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);

client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");

// Then a `FULLY_READ` notable update is received,
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::FULLY_READ), "{received_reasons:?}");
}
);

let room = client.get_room(room_id).expect("room should exist");
assert_eq!(room.fully_read_event_id().as_deref().map(|id| id.as_str()), Some("$first"),);

// But getting the same value again won't trigger a new notable update…
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");

assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::FULLY_READ), "{received_reasons:?}");
}
);
assert!(room_info_notable_update_stream.is_empty());

// … Unless the event ID changes!
let room_account_data_events = vec![
Raw::from_json_string(
json!({
"type": "m.fully_read",
"content": { "event_id": "$second" },
})
.to_string(),
)
.unwrap(),
];
response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");

assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::FULLY_READ), "{received_reasons:?}");
}
);
assert_eq!(room.fully_read_event_id().as_deref().map(|id| id.as_str()), Some("$second"),);
assert!(room_info_notable_update_stream.is_empty());
}

#[async_test]
async fn test_unstable_unread_marker_is_ignored_after_stable() {
// Given a logged-in client,
Expand Down
Loading