Skip to content

Commit a80fa91

Browse files
danderson-contstefanceriu
authored andcommitted
feat(base): expose m.fully_read event ID on RoomInfo
Persist `m.fully_read` on `BaseRoomInfo` and expose it on `RoomInfo::fully_read_event_id` and `Room::fully_read_event_id`. The `m.fully_read` response processor now stores the event ID on the room info and emits `RoomInfoNotableUpdateReasons::FULLY_READ` Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>
1 parent 4b1ae06 commit a80fa91

5 files changed

Lines changed: 178 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `RoomInfo::fully_read_event_id` and `Room::fully_read_event_id` to expose the user's `m.fully_read` event ID.

crates/matrix-sdk-base/src/response_processors/account_data/room.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414

1515
use ruma::{
1616
RoomId,
17-
events::{AnyRoomAccountDataEvent, marked_unread::MarkedUnreadEventContent},
17+
events::{
18+
AnyRoomAccountDataEvent, fully_read::FullyReadEventContent,
19+
marked_unread::MarkedUnreadEventContent,
20+
},
1821
serde::Raw,
1922
};
2023
use tracing::{instrument, warn};
@@ -85,6 +88,21 @@ pub fn for_room(
8588
},
8689
);
8790
}
91+
AnyRoomAccountDataEvent::FullyRead(event) => {
92+
on_room_info(
93+
room_id,
94+
&mut context.state_changes,
95+
state_store,
96+
|room_info| {
97+
on_fully_read_marker(
98+
room_id,
99+
&event.content,
100+
room_info,
101+
&mut context.room_info_notable_updates,
102+
);
103+
},
104+
);
105+
}
88106

89107
// Nothing.
90108
_ => {}
@@ -128,6 +146,25 @@ fn on_room_info<F>(
128146
}
129147
}
130148

149+
// Helper to update the fully-read marker event id on the `RoomInfo` and
150+
// notify subscribers when the value changes.
151+
fn on_fully_read_marker(
152+
room_id: &RoomId,
153+
content: &FullyReadEventContent,
154+
room_info: &mut RoomInfo,
155+
room_info_notable_updates: &mut RoomInfoNotableUpdates,
156+
) {
157+
if room_info.base_info.fully_read_event_id.as_ref() == Some(&content.event_id) {
158+
return;
159+
}
160+
161+
room_info.base_info.fully_read_event_id = Some(content.event_id.clone());
162+
room_info_notable_updates
163+
.entry(room_id.to_owned())
164+
.or_default()
165+
.insert(RoomInfoNotableUpdateReasons::FULLY_READ);
166+
}
167+
131168
// Helper to update the unread marker for stable and unstable prefixes.
132169
fn on_unread_marker(
133170
room_id: &RoomId,

crates/matrix-sdk-base/src/room/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,12 @@ impl Room {
513513
self.info.read().base_info.is_marked_unread
514514
}
515515

516+
/// Returns the event ID of the user's `m.fully_read` marker for this room,
517+
/// if any.
518+
pub fn fully_read_event_id(&self) -> Option<OwnedEventId> {
519+
self.info.read().fully_read_event_id().map(ToOwned::to_owned)
520+
}
521+
516522
/// Returns the [`RoomVersionId`] of the room, if known.
517523
pub fn version(&self) -> Option<RoomVersionId> {
518524
self.info.read().room_version().cloned()

crates/matrix-sdk-base/src/room/room_info.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ pub struct BaseRoomInfo {
220220
/// others, and this field collects them.
221221
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
222222
pub(crate) notable_tags: RoomNotableTags,
223+
/// The event ID of the user's `m.fully_read` marker for this room, if any.
224+
#[serde(skip_serializing_if = "Option::is_none", default)]
225+
pub(crate) fully_read_event_id: Option<OwnedEventId>,
223226
/// The `m.room.pinned_events` of this room.
224227
pub(crate) pinned_events: Option<PossiblyRedactedRoomPinnedEventsEventContent>,
225228
}
@@ -557,6 +560,7 @@ impl Default for BaseRoomInfo {
557560
is_marked_unread: false,
558561
is_marked_unread_source: AccountDataSource::Unstable,
559562
notable_tags: RoomNotableTags::empty(),
563+
fully_read_event_id: None,
560564
pinned_events: None,
561565
}
562566
}
@@ -1225,6 +1229,12 @@ impl RoomInfo {
12251229
self.base_info.pinned_events.clone().and_then(|c| c.pinned)
12261230
}
12271231

1232+
/// Returns the event ID of the user's `m.fully_read` marker for this room,
1233+
/// if any.
1234+
pub fn fully_read_event_id(&self) -> Option<&EventId> {
1235+
self.base_info.fully_read_event_id.as_deref()
1236+
}
1237+
12281238
/// Checks if an `EventId` is currently pinned.
12291239
/// It avoids having to clone the whole list of event ids to check a single
12301240
/// value.

crates/matrix-sdk-base/src/sliding_sync.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,6 +1885,129 @@ mod tests {
18851885
assert!(room_info_notable_update_stream.is_empty());
18861886
}
18871887

1888+
#[async_test]
1889+
async fn test_fully_read_marker_can_trigger_a_notable_update_reason() {
1890+
// Given a logged-in client,
1891+
let client = logged_in_base_client(None).await;
1892+
let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
1893+
1894+
// When I receive a sliding sync response containing a new room,
1895+
let room_id = room_id!("!r:e.uk");
1896+
let room = http::response::Room::new();
1897+
let response = response_with_room(room_id, room);
1898+
client
1899+
.process_sliding_sync(
1900+
&response,
1901+
&RequestedRequiredStates::default(),
1902+
&client.state_store_lock().lock().await,
1903+
)
1904+
.await
1905+
.expect("Failed to process sync");
1906+
1907+
// Other notable updates are received, but not the ones we are interested by.
1908+
assert_matches!(
1909+
room_info_notable_update_stream.recv().await,
1910+
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
1911+
assert_eq!(received_room_id, room_id);
1912+
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
1913+
}
1914+
);
1915+
assert_matches!(
1916+
room_info_notable_update_stream.recv().await,
1917+
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
1918+
assert_eq!(received_room_id, room_id);
1919+
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
1920+
}
1921+
);
1922+
assert!(room_info_notable_update_stream.is_empty());
1923+
1924+
// When I receive a sliding sync response containing an `m.fully_read`
1925+
// account-data event,
1926+
let room_account_data_events = vec![
1927+
Raw::from_json_string(
1928+
json!({
1929+
"type": "m.fully_read",
1930+
"content": { "event_id": "$first" },
1931+
})
1932+
.to_string(),
1933+
)
1934+
.unwrap(),
1935+
];
1936+
let mut response = response_with_room(room_id, http::response::Room::new());
1937+
response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);
1938+
1939+
client
1940+
.process_sliding_sync(
1941+
&response,
1942+
&RequestedRequiredStates::default(),
1943+
&client.state_store_lock().lock().await,
1944+
)
1945+
.await
1946+
.expect("Failed to process sync");
1947+
1948+
// Then a `FULLY_READ` notable update is received,
1949+
assert_matches!(
1950+
room_info_notable_update_stream.recv().await,
1951+
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
1952+
assert_eq!(received_room_id, room_id);
1953+
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::FULLY_READ), "{received_reasons:?}");
1954+
}
1955+
);
1956+
1957+
let room = client.get_room(room_id).expect("room should exist");
1958+
assert_eq!(room.fully_read_event_id().as_deref().map(|id| id.as_str()), Some("$first"),);
1959+
1960+
// But getting the same value again won't trigger a new notable update…
1961+
client
1962+
.process_sliding_sync(
1963+
&response,
1964+
&RequestedRequiredStates::default(),
1965+
&client.state_store_lock().lock().await,
1966+
)
1967+
.await
1968+
.expect("Failed to process sync");
1969+
1970+
assert_matches!(
1971+
room_info_notable_update_stream.recv().await,
1972+
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
1973+
assert_eq!(received_room_id, room_id);
1974+
assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::FULLY_READ), "{received_reasons:?}");
1975+
}
1976+
);
1977+
assert!(room_info_notable_update_stream.is_empty());
1978+
1979+
// … Unless the event ID changes!
1980+
let room_account_data_events = vec![
1981+
Raw::from_json_string(
1982+
json!({
1983+
"type": "m.fully_read",
1984+
"content": { "event_id": "$second" },
1985+
})
1986+
.to_string(),
1987+
)
1988+
.unwrap(),
1989+
];
1990+
response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);
1991+
client
1992+
.process_sliding_sync(
1993+
&response,
1994+
&RequestedRequiredStates::default(),
1995+
&client.state_store_lock().lock().await,
1996+
)
1997+
.await
1998+
.expect("Failed to process sync");
1999+
2000+
assert_matches!(
2001+
room_info_notable_update_stream.recv().await,
2002+
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
2003+
assert_eq!(received_room_id, room_id);
2004+
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::FULLY_READ), "{received_reasons:?}");
2005+
}
2006+
);
2007+
assert_eq!(room.fully_read_event_id().as_deref().map(|id| id.as_str()), Some("$second"),);
2008+
assert!(room_info_notable_update_stream.is_empty());
2009+
}
2010+
18882011
#[async_test]
18892012
async fn test_unstable_unread_marker_is_ignored_after_stable() {
18902013
// Given a logged-in client,

0 commit comments

Comments
 (0)