From 5932efa92d86a72593ed59fc29a0c441d0c348e1 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Mon, 26 May 2025 17:01:26 +0100 Subject: [PATCH] feat: Typing notifications in simplified sliding sync What's missing? Being able to use separate rooms & lists for typing indicators. At the moment, we use the same ones as we use for the timeline, as todo_rooms is quite intertwined. We need to disentangle this to get that functionality, although I'm not sure if clients use it. --- src/api/client/sync/v3/joined.rs | 2 +- src/api/client/sync/v5.rs | 62 ++++++++++++++++++++++++++++++++ src/service/rooms/typing/mod.rs | 22 ++++++++---- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/api/client/sync/v3/joined.rs b/src/api/client/sync/v3/joined.rs index 42ff2d5c..7324df97 100644 --- a/src/api/client/sync/v3/joined.rs +++ b/src/api/client/sync/v3/joined.rs @@ -370,7 +370,7 @@ pub(super) async fn load_joined_room( let typings = services .rooms .typing - .typings_all(room_id, sender_user) + .typings_event_for_user(room_id, sender_user) .await?; Ok(vec![serde_json::from_str(&serde_json::to_string(&typings)?)?]) diff --git a/src/api/client/sync/v5.rs b/src/api/client/sync/v5.rs index 76318841..24180b35 100644 --- a/src/api/client/sync/v5.rs +++ b/src/api/client/sync/v5.rs @@ -31,6 +31,7 @@ use ruma::{ events::{ AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType, room::member::{MembershipState, RoomMemberEventContent}, + typing::TypingEventContent, }, serde::Raw, uint, @@ -212,6 +213,9 @@ pub(crate) async fn sync_events_v5_route( _ = tokio::time::timeout(duration, watcher).await; } + let typing = collect_typing_events(services, sender_user, &body, &todo_rooms).await?; + response.extensions.typing = typing; + trace!( rooms = ?response.rooms.len(), account_data = ?response.extensions.account_data.rooms.len(), @@ -295,6 +299,8 @@ where Rooms: Iterator + Clone + Send + 'a, AllRooms: Iterator + Clone + Send + 'a, { + // TODO MSC4186: Implement remaining list filters: is_dm, is_encrypted, + // room_types. for (list_id, list) in &body.lists { let active_rooms: Vec<_> = match list.filters.as_ref().and_then(|f| f.is_invite) { | None => all_rooms.clone().collect(), @@ -674,6 +680,62 @@ where } Ok(rooms) } + +async fn collect_typing_events( + services: &Services, + sender_user: &UserId, + body: &sync_events::v5::Request, + todo_rooms: &TodoRooms, +) -> Result { + if !body.extensions.typing.enabled.unwrap_or(false) { + return Ok(sync_events::v5::response::Typing::default()); + } + let rooms: Vec<_> = body.extensions.typing.rooms.clone().unwrap_or_else(|| { + body.room_subscriptions + .keys() + .map(ToOwned::to_owned) + .collect() + }); + let lists: Vec<_> = body + .extensions + .typing + .lists + .clone() + .unwrap_or_else(|| body.lists.keys().map(ToOwned::to_owned).collect::>()); + + if rooms.is_empty() && lists.is_empty() { + return Ok(sync_events::v5::response::Typing::default()); + } + + let mut typing_response = sync_events::v5::response::Typing::default(); + for (room_id, (required_state_request, timeline_limit, roomsince)) in todo_rooms { + if services.rooms.typing.last_typing_update(room_id).await? <= *roomsince { + continue; + } + + match services + .rooms + .typing + .typing_users_for_user(room_id, sender_user) + .await + { + | Ok(typing_users) => { + typing_response.rooms.insert( + room_id.to_owned(), // Already OwnedRoomId + Raw::new(&sync_events::v5::response::SyncTypingEvent { + content: TypingEventContent::new(typing_users), + })?, + ); + }, + | Err(e) => { + warn!(%room_id, "Failed to get typing events for room: {}", e); + }, + } + } + + Ok(typing_response) +} + async fn collect_account_data( services: &Services, (sender_user, _, globalsince, body): (&UserId, &DeviceId, u64, &sync_events::v5::Request), diff --git a/src/service/rooms/typing/mod.rs b/src/service/rooms/typing/mod.rs index a81ee95c..28b9dfa7 100644 --- a/src/service/rooms/typing/mod.rs +++ b/src/service/rooms/typing/mod.rs @@ -179,18 +179,15 @@ impl Service { .unwrap_or(0)) } - /// Returns a new typing EDU. - pub async fn typings_all( + pub async fn typing_users_for_user( &self, room_id: &RoomId, sender_user: &UserId, - ) -> Result> { + ) -> Result> { let room_typing_indicators = self.typing.read().await.get(room_id).cloned(); let Some(typing_indicators) = room_typing_indicators else { - return Ok(SyncEphemeralRoomEvent { - content: ruma::events::typing::TypingEventContent { user_ids: Vec::new() }, - }); + return Ok(Vec::new()); }; let user_ids: Vec<_> = typing_indicators @@ -207,8 +204,19 @@ impl Service { .collect() .await; + Ok(user_ids) + } + + /// Returns a new typing EDU. + pub async fn typings_event_for_user( + &self, + room_id: &RoomId, + sender_user: &UserId, + ) -> Result> { Ok(SyncEphemeralRoomEvent { - content: ruma::events::typing::TypingEventContent { user_ids }, + content: ruma::events::typing::TypingEventContent { + user_ids: self.typing_users_for_user(room_id, sender_user).await?, + }, }) }