Skip to content
Open
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
3 changes: 2 additions & 1 deletion bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2223,7 +2223,7 @@ async fn notification_handler(
.map(ToString::to_string)
.collect(),
active_service_members_count: room
.active_service_members()
.update_active_service_members()
.await
.ok()
.flatten()
Expand All @@ -2232,6 +2232,7 @@ async fn notification_handler(
is_encrypted: Some(room.encryption_state().is_encrypted()),
is_direct,
is_space: room.is_space(),
is_dm: room.compute_is_dm().await.ok().unwrap_or_default(),
};

listener.on_notification(
Expand Down
2 changes: 2 additions & 0 deletions bindings/matrix-sdk-ffi/src/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub struct NotificationRoomInfo {
pub is_encrypted: Option<bool>,
pub is_direct: bool,
pub is_space: bool,
pub is_dm: bool,
}

#[derive(uniffi::Record)]
Expand Down Expand Up @@ -113,6 +114,7 @@ impl NotificationItem {
is_encrypted: item.is_room_encrypted,
is_direct: item.is_direct_message_room,
is_space: item.is_space,
is_dm: item.room_is_dm,
},
is_noisy: item.is_noisy,
has_mention: item.has_mention,
Expand Down
7 changes: 2 additions & 5 deletions bindings/matrix-sdk-ffi/src/room/room_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,6 @@ impl RoomInfo {
.ok()
.map(|p| RoomPowerLevels::new(p, room.own_user_id().to_owned()));

let active_service_members_count =
room.active_service_members().await?.unwrap_or_default().len() as u64;

Ok(Self {
id: room.room_id().to_string(),
encryption_state: room.encryption_state(),
Expand All @@ -161,7 +158,7 @@ impl RoomInfo {
topic: room.topic(),
avatar_url: room.avatar_url().map(Into::into),
is_direct: room.is_direct().await?,
is_dm: room.is_dm().await?,
is_dm: room.compute_is_dm().await?,
is_public: room.is_public(),
is_space: room.is_space(),
successor_room: room.successor_room().map(Into::into),
Expand All @@ -186,7 +183,7 @@ impl RoomInfo {
active_members_count: room.active_members_count(),
invited_members_count: room.invited_members_count(),
joined_members_count: room.joined_members_count(),
active_service_members_count,
active_service_members_count: room.active_service_members_count().unwrap_or_default(),
service_members: room
.service_members()
.iter()
Expand Down
5 changes: 5 additions & 0 deletions bindings/matrix-sdk-ffi/src/spaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ pub struct SpaceRoom {
pub heroes: Option<Vec<RoomHero>>,
/// The via parameters of the room.
pub via: Vec<String>,
/// Whether this room is a DM, if known.
/// Note this value can be calculated following some assumptions and is not
/// guaranteed to be accurate.
pub is_dm: Option<bool>,
}

impl From<UISpaceRoom> for SpaceRoom {
Expand All @@ -380,6 +384,7 @@ impl From<UISpaceRoom> for SpaceRoom {
state: room.state.map(Into::into),
heroes: room.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
via: room.via.into_iter().map(Into::into).collect(),
is_dm: room.is_dm,
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/matrix-sdk-base/src/room/display_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ pub(crate) struct RoomSummary {
pub joined_member_count: u64,
/// The number of members that are considered to be invited to the room.
pub invited_member_count: u64,
/// The number of active (joined/invited) service members in the room, if
/// known.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_service_members: Option<u64>,
}

#[cfg(test)]
Expand Down
45 changes: 39 additions & 6 deletions crates/matrix-sdk-base/src/room/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub use state::{RoomState, RoomStateFilter};
pub(crate) use tags::RoomNotableTags;
use tokio::sync::broadcast;
pub use tombstone::{PredecessorRoom, SuccessorRoom};
use tracing::{info, instrument, warn};
use tracing::{info, instrument, trace, warn};

use crate::{
DmRoomDefinition, Error,
Expand Down Expand Up @@ -304,9 +304,9 @@ impl Room {
}
}

/// Checks if the current room is a DM based on the rules from the
/// [`DmRoomDefinition`].
pub async fn is_dm(&self, dm_room_definition: &DmRoomDefinition) -> StoreResult<bool> {
/// Computes if the current room is a DM based on the rules from the
/// [`DmRoomDefinition`], updating the active service members.
pub async fn compute_is_dm(&self, dm_room_definition: &DmRoomDefinition) -> StoreResult<bool> {
let is_direct = self.is_direct().await?;

match *dm_room_definition {
Expand All @@ -316,7 +316,7 @@ impl Room {
return Ok(false);
}
let active_service_member_count =
self.active_service_members().await?.unwrap_or_default().len() as u64;
self.update_active_service_members().await?.unwrap_or_default().len() as u64;
let has_at_most_two_members =
self.active_members_count().saturating_sub(active_service_member_count) <= 2;
Ok(has_at_most_two_members)
Expand Down Expand Up @@ -544,7 +544,7 @@ impl Room {
/// Returns the list of service members that are either in a joined or
/// invited state in this room, checking the service member list against the
/// locally available room members.
pub async fn active_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
pub async fn update_active_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
if let Some(service_members) = self.service_members() {
let mut found = Vec::new();
for user_id in service_members {
Expand All @@ -563,11 +563,44 @@ impl Room {
}
}

trace!(
"Updating active service members ({}) in room {:?}",
found.len(),
self.room_id()
);

let new_active_service_member_count = found.len() as u64;
let current_active_service_member_count =
self.info.read().summary.active_service_members.unwrap_or_default();
if new_active_service_member_count != current_active_service_member_count {
let mut new_room_info = self.clone_info();
new_room_info
.update_active_service_member_count(Some(new_active_service_member_count));
self.set_room_info(
new_room_info,
RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS,
);
}

Ok(Some(found))
} else {
if self.info.read().summary.active_service_members.is_some() {
let mut new_room_info = self.clone_info();
new_room_info.update_active_service_member_count(None);
self.set_room_info(
new_room_info,
RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS,
);
}
Ok(None)
}
}

/// Returns a cached value containing the active (joined/invited) service
/// member count, if known.
pub fn active_service_members_count(&self) -> Option<u64> {
self.info.read().summary.active_service_members
}
}

// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
Expand Down
137 changes: 134 additions & 3 deletions crates/matrix-sdk-base/src/room/room_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,24 @@ impl RoomInfo {
&mut self,
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
) -> bool {
// Store the state event in the `BaseRoomInfo` first.
// When we receive a `m.room.member_hints` event
if raw_event.event_type == StateEventType::MemberHints
&& let Some(AnySyncStateEvent::MemberHints(new_hints)) = raw_event.deserialize()
// If we have both old and new member hints events
&& let (Some(current_hints), Some(new)) =
(&self.base_info.member_hints, new_hints.as_original())
// Then we check if their contents don't match
&& current_hints
.content
.service_members
.as_ref()
.is_some_and(|current_members| *current_members != new.content.service_members)
{
// And reset the computed value in that case
self.summary.active_service_members = None;
}

// Store the state event in the `BaseRoomInfo`.
let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);

if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
Expand Down Expand Up @@ -872,6 +889,10 @@ impl RoomInfo {
}
}

if changed {
self.summary.active_service_members = None;
}

changed
}

Expand Down Expand Up @@ -1233,6 +1254,18 @@ impl RoomInfo {

migrated
}

/// Returns the number of active (joined/invited) service members in the
/// room, if known.
pub fn active_service_member_count(&self) -> Option<u64> {
self.summary.active_service_members
}

/// Updates the cached value for the number of active service members in the
/// room.
pub fn update_active_service_member_count(&mut self, count: Option<u64>) {
self.summary.active_service_members = count;
}
}

/// Type to represent a `RoomInfo::recency_stamp`.
Expand Down Expand Up @@ -1346,6 +1379,9 @@ bitflags! {
/// The display name has changed.
const DISPLAY_NAME = 0b0010_0000;

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

/// This is a temporary hack.
///
/// So here is the thing. Ideally, we DO NOT want to emit this reason. It does not
Expand All @@ -1368,7 +1404,7 @@ impl Default for RoomInfoNotableUpdateReasons {

#[cfg(test)]
mod tests {
use std::{str::FromStr, sync::Arc};
use std::{collections::BTreeSet, str::FromStr, sync::Arc};

use assert_matches::assert_matches;
use matrix_sdk_test::{async_test, event_factory::EventFactory};
Expand All @@ -1388,7 +1424,7 @@ mod tests {

use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
use crate::{
RoomDisplayName, RoomHero, RoomState, StateChanges,
RawStateEventWithKeys, RoomDisplayName, RoomHero, RoomState, StateChanges,
notification_settings::RoomNotificationMode,
room::{RoomNotableTags, RoomSummary},
store::{IntoStateStore, MemoryStore},
Expand Down Expand Up @@ -1416,6 +1452,7 @@ mod tests {
}],
joined_member_count: 5,
invited_member_count: 0,
active_service_members: None,
},
members_synced: true,
last_prev_batch: Some("pb".to_owned()),
Expand Down Expand Up @@ -1761,4 +1798,98 @@ mod tests {
assert!(info.base_info.tombstone.is_none());
assert!(info.base_info.topic.is_none());
}

#[test]
fn test_member_hints_with_different_contents_reset_computed_value() {
let expected = BTreeSet::from_iter([
owned_user_id!("@alice:example.org"),
owned_user_id!("@bob:example.org"),
]);

let info_json = json!({
"room_id": "!gda78o:server.tld",
"room_state": "Invited",
"notification_counts": {
"highlight_count": 1,
"notification_count": 2,
},
"summary": {
"room_heroes": [{
"user_id": "@somebody:example.org",
"display_name": "Somebody",
"avatar_url": "mxc://example.org/abc"
}],
"joined_member_count": 5,
"invited_member_count": 0,
"active_service_members": 2,
},
"members_synced": true,
"last_prev_batch": "pb",
"sync_info": "FullySynced",
"encryption_state_synced": true,
"base_info": {
"avatar": null,
"canonical_alias": null,
"create": null,
"dm_targets": [],
"encryption": null,
"guest_access": null,
"history_visibility": null,
"join_rules": null,
"max_power_level": 100,
"member_hints": {
"Original": {
"content": {
"service_members": ["@alice:example.org", "@bob:example.org"]
}
}
},
"name": null,
"tombstone": null,
"topic": null,
},
});

let info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
assert_eq!(info.summary.active_service_members, Some(2));

// We receive a new event with the same values as the stored ones
let mut info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
EventFactory::new()
.sender(user_id!("@alice:example.org"))
.member_hints(expected.clone())
.into_raw_sync_state(),
)
.expect("Expected member hints event is created");

info.handle_state_event(&mut raw_state_event_with_keys);

// Nothing changed
assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
// And the computed value is kept
assert_eq!(info.summary.active_service_members, Some(2));

// We receive a new event with different values from the stored ones
let mut info: RoomInfo = serde_json::from_value(info_json).unwrap();
let new_member_hints = BTreeSet::from_iter([owned_user_id!("@alice:example.org")]);
let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
EventFactory::new()
.sender(user_id!("@alice:example.org"))
.member_hints(new_member_hints.clone())
.into_raw_sync_state(),
)
.expect("New member hints event is created");

info.handle_state_event(&mut raw_state_event_with_keys);

// The new member hints were applied
assert_eq!(
info.base_info.member_hints.unwrap().content.service_members.unwrap(),
new_member_hints
);
// And the computed value is reset
assert!(info.summary.active_service_members.is_none());
}
}
6 changes: 5 additions & 1 deletion crates/matrix-sdk-ui/src/notification_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,9 @@ pub struct NotificationItem {

/// The push actions for this notification (notify, sound, highlight, etc.).
pub actions: Option<Vec<Action>>,

/// Whether the room this notification is from is a DM or not.
pub room_is_dm: bool,
}

impl NotificationItem {
Expand Down Expand Up @@ -1034,7 +1037,7 @@ impl NotificationItem {
.collect_vec();

let active_service_members_count =
room.active_service_members().await?.unwrap_or_default().len() as u64;
room.update_active_service_members().await?.unwrap_or_default().len() as u64;

let item = NotificationItem {
event,
Expand All @@ -1061,6 +1064,7 @@ impl NotificationItem {
has_mention,
thread_id,
actions: push_actions.map(|actions| actions.to_vec()),
room_is_dm: room.compute_is_dm().await?,
};

Ok(item)
Expand Down
Loading
Loading