From 985d1758628051939cabee4cfe2160f015a671a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 25 Oct 2024 13:49:26 +0200 Subject: [PATCH 1/3] feat(room_list): allow knock state event as `latest_event` This allows clients to display pending knocking requests in the room list items. --- crates/matrix-sdk-base/src/client.rs | 3 +- crates/matrix-sdk-base/src/latest_event.rs | 24 +++++++- .../matrix-sdk-base/src/sliding_sync/mod.rs | 60 ++++++++++++++++++- .../src/timeline/event_item/content/mod.rs | 22 ++++++- .../src/timeline/event_item/mod.rs | 43 ++++++++++++- 5 files changed, 143 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index fdb798fdc09..ecc3a81a873 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -807,7 +807,8 @@ impl BaseClient { | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) | PossibleLatestEvent::YesCallNotify(_) - | PossibleLatestEvent::YesSticker(_) => { + | PossibleLatestEvent::YesSticker(_) + | PossibleLatestEvent::YesKnockedStateEvent(_) => { // The event is the right type for us to use as latest_event return Some((Box::new(LatestEvent::new(decrypted)), i)); } diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index ecdd490dccf..ed94c447a00 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -12,7 +12,14 @@ use ruma::events::{ room::message::SyncRoomMessageEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, }; -use ruma::{events::sticker::SyncStickerEvent, MxcUri, OwnedEventId}; +use ruma::{ + events::{ + room::member::{MembershipState, SyncRoomMemberEvent}, + sticker::SyncStickerEvent, + AnySyncStateEvent, + }, + MxcUri, OwnedEventId, +}; use serde::{Deserialize, Serialize}; use crate::MinimalRoomMemberEvent; @@ -37,6 +44,9 @@ pub enum PossibleLatestEvent<'a> { /// This message is suitable - it's a call notification YesCallNotify(&'a SyncCallNotifyEvent), + /// This state event is suitable - it's a knock membership change + YesKnockedStateEvent(&'a SyncRoomMemberEvent), + // Later: YesState(), // Later: YesReaction(), /// Not suitable - it's a state event @@ -102,8 +112,16 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat // suitable AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType, - // We don't currently support state events - AnySyncTimelineEvent::State(_) => PossibleLatestEvent::NoUnsupportedEventType, + // We don't currently support most state events + AnySyncTimelineEvent::State(state) => { + // But we make an exception for knocked state events + if let AnySyncStateEvent::RoomMember(member) = state { + if matches!(member.membership(), MembershipState::Knock) { + return PossibleLatestEvent::YesKnockedStateEvent(member); + } + } + PossibleLatestEvent::NoUnsupportedEventType + } } } diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 17efc70323e..a6f711b0d28 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -705,7 +705,8 @@ async fn cache_latest_events( | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) | PossibleLatestEvent::YesCallNotify(_) - | PossibleLatestEvent::YesSticker(_) => { + | PossibleLatestEvent::YesSticker(_) + | PossibleLatestEvent::YesKnockedStateEvent(_) => { // We found a suitable latest event. Store it. // In order to make the latest event fast to read, we want to keep the @@ -1738,6 +1739,63 @@ mod tests { ); } + #[async_test] + async fn test_last_knock_member_state_event_from_sliding_sync_is_cached() { + // Given a logged-in client + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + // And a knock member state event + let knock_event = json!({ + "sender":"@alice:example.com", + "state_key":"@alice:example.com", + "type":"m.room.member", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content":{"membership": "knock"}, + "room_id": room_id, + }); + + // When the sliding sync response contains a timeline + let events = &[knock_event]; + let room = room_with_timeline(events); + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // Then the room holds the latest knock state event + let client_room = client.get_room(room_id).expect("No room found"); + assert_eq!( + ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())), + "$ida" + ); + } + + #[async_test] + async fn test_last_member_state_event_from_sliding_sync_is_not_cached() { + // Given a logged-in client + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + // And a join member state event + let join_event = json!({ + "sender":"@alice:example.com", + "state_key":"@alice:example.com", + "type":"m.room.member", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content":{"membership": "join"}, + "room_id": room_id, + }); + + // When the sliding sync response contains a timeline + let events = &[join_event]; + let room = room_with_timeline(events); + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // Then the room doesn't hold the join state event as the latest event + let client_room = client.get_room(room_id).expect("No room found"); + assert!(client_room.latest_event().is_none()); + } + #[async_test] async fn test_cached_latest_event_can_be_redacted() { // Given a logged-in client diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 7df383701b6..d85b4d2c448 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -39,7 +39,7 @@ use ruma::{ guest_access::RoomGuestAccessEventContent, history_visibility::RoomHistoryVisibilityEventContent, join_rules::RoomJoinRulesEventContent, - member::{Change, RoomMemberEventContent}, + member::{Change, RoomMemberEventContent, SyncRoomMemberEvent}, message::{ Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, SyncRoomMessageEvent, @@ -173,6 +173,9 @@ impl TimelineItemContent { ); None } + PossibleLatestEvent::YesKnockedStateEvent(member) => { + Some(Self::from_suitable_latest_knock_state_event_content(member)) + } PossibleLatestEvent::NoEncrypted => { warn!("Found an encrypted event cached as latest_event! ID={}", event.event_id()); None @@ -220,6 +223,23 @@ impl TimelineItemContent { } } + fn from_suitable_latest_knock_state_event_content( + event: &SyncRoomMemberEvent, + ) -> TimelineItemContent { + match event { + SyncRoomMemberEvent::Original(event) => { + let content = event.content.clone(); + let prev_content = event.prev_content().cloned(); + TimelineItemContent::room_member( + event.state_key.to_owned(), + FullStateEventContent::Original { content, prev_content }, + event.sender.to_owned(), + ) + } + SyncRoomMemberEvent::Redacted(_) => TimelineItemContent::RedactedMessage, + } + } + /// Given some sticker content that is from an event that we have already /// determined is suitable for use as a latest event in a message preview, /// extract its contents and wrap it as a `TimelineItemContent`. diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 9c28bad5b59..c94c858cdf7 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -733,7 +733,7 @@ mod tests { }; use super::{EventTimelineItem, Profile}; - use crate::timeline::TimelineDetails; + use crate::timeline::{MembershipChange, TimelineDetails, TimelineItemContent}; #[async_test] async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() { @@ -764,6 +764,41 @@ mod tests { } } + #[async_test] + async fn test_latest_knock_member_state_event_can_be_wrapped_as_a_timeline_item() { + // Given a sync knock member state event that is suitable to be used as a + // latest_event + + let room_id = room_id!("!q:x.uk"); + let user_id = user_id!("@t:o.uk"); + let raw_event = member_event_as_state_event( + room_id, + user_id, + "knock", + "Alice Margatroid", + "mxc://e.org/SEs", + ); + let client = logged_in_client(None).await; + + // When we construct a timeline event from it + let event = SyncTimelineEvent::new(raw_event.cast()); + let timeline_item = + EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) + .await + .unwrap(); + + // Then its properties correctly translate + assert_eq!(timeline_item.sender, user_id); + assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable); + assert_eq!(timeline_item.timestamp.0, UInt::new(143273583).unwrap()); + if let TimelineItemContent::MembershipChange(change) = timeline_item.content { + assert_eq!(change.user_id, user_id); + assert_matches!(change.change, Some(MembershipChange::Knocked)); + } else { + panic!("Unexpected state event type"); + } + } + #[async_test] async fn test_latest_message_includes_bundled_edit() { // Given a sync event that is suitable to be used as a latest_event, and @@ -885,6 +920,7 @@ mod tests { room.required_state.push(member_event_as_state_event( room_id, user_id, + "join", "Alice Margatroid", "mxc://e.org/SEs", )); @@ -987,6 +1023,7 @@ mod tests { fn member_event_as_state_event( room_id: &RoomId, user_id: &UserId, + membership: &str, display_name: &str, avatar_url: &str, ) -> Raw { @@ -995,13 +1032,13 @@ mod tests { "content": { "avatar_url": avatar_url, "displayname": display_name, - "membership": "join", + "membership": membership, "reason": "" }, "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 143273583, "room_id": room_id, - "sender": "@example:example.org", + "sender": user_id, "state_key": user_id, "type": "m.room.member", "unsigned": { From 682a3738ee85f6a9b6fe33812c95273d41e785aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 28 Oct 2024 13:23:58 +0100 Subject: [PATCH 2/3] refactor(room_list): only display the knock state events if the current user can act on them That is, if their power level allows them to either invite or kick users. --- bindings/matrix-sdk-ffi/src/room.rs | 2 +- crates/matrix-sdk-base/src/client.rs | 5 +- crates/matrix-sdk-base/src/error.rs | 8 ++ crates/matrix-sdk-base/src/latest_event.rs | 53 +++++++---- crates/matrix-sdk-base/src/rooms/normal.rs | 14 ++- .../matrix-sdk-base/src/sliding_sync/mod.rs | 87 +++++++++++++++++-- .../src/timeline/event_item/content/mod.rs | 5 +- .../src/timeline/event_item/mod.rs | 33 ++++++- crates/matrix-sdk/src/room/mod.rs | 28 +++--- .../tests/integration/room/joined.rs | 2 +- 10 files changed, 193 insertions(+), 44 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 9f11366a53c..9b01c4f6814 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -634,7 +634,7 @@ impl Room { } pub async fn get_power_levels(&self) -> Result { - let power_levels = self.inner.room_power_levels().await?; + let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?; Ok(RoomPowerLevels::from(power_levels)) } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index ecc3a81a873..1033345a80b 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -789,6 +789,8 @@ impl BaseClient { room: &Room, ) -> Option<(Box, usize)> { let enc_events = room.latest_encrypted_events(); + let power_levels = room.power_levels().await.ok(); + let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref()); // Walk backwards through the encrypted events, looking for one we can decrypt for (i, event) in enc_events.iter().enumerate().rev() { @@ -802,14 +804,13 @@ impl BaseClient { // We found an event we can decrypt if let Ok(any_sync_event) = decrypted.raw().deserialize() { // We can deserialize it to find its type - match is_suitable_for_latest_event(&any_sync_event) { + match is_suitable_for_latest_event(&any_sync_event, power_levels_info) { PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) | PossibleLatestEvent::YesCallNotify(_) | PossibleLatestEvent::YesSticker(_) | PossibleLatestEvent::YesKnockedStateEvent(_) => { - // The event is the right type for us to use as latest_event return Some((Box::new(LatestEvent::new(decrypted)), i)); } _ => (), diff --git a/crates/matrix-sdk-base/src/error.rs b/crates/matrix-sdk-base/src/error.rs index bd7a8f1f1a1..8d1a2dd3e22 100644 --- a/crates/matrix-sdk-base/src/error.rs +++ b/crates/matrix-sdk-base/src/error.rs @@ -61,4 +61,12 @@ pub enum Error { /// function with invalid parameters #[error("receive_all_members function was called with invalid parameters")] InvalidReceiveMembersParameters, + + /// This request failed because the local data wasn't sufficient. + #[error("Local cache doesn't contain all necessary data to perform the action.")] + InsufficientData, + + /// There was a [`serde_json`] deserialization error. + #[error(transparent)] + DeserializationError(#[from] serde_json::error::Error), } diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index ed94c447a00..4b2f1ff9b13 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -14,11 +14,14 @@ use ruma::events::{ }; use ruma::{ events::{ - room::member::{MembershipState, SyncRoomMemberEvent}, + room::{ + member::{MembershipState, SyncRoomMemberEvent}, + power_levels::RoomPowerLevels, + }, sticker::SyncStickerEvent, AnySyncStateEvent, }, - MxcUri, OwnedEventId, + MxcUri, OwnedEventId, UserId, }; use serde::{Deserialize, Serialize}; @@ -45,6 +48,7 @@ pub enum PossibleLatestEvent<'a> { YesCallNotify(&'a SyncCallNotifyEvent), /// This state event is suitable - it's a knock membership change + /// that can be handled by the current user. YesKnockedStateEvent(&'a SyncRoomMemberEvent), // Later: YesState(), @@ -60,7 +64,10 @@ pub enum PossibleLatestEvent<'a> { /// Decide whether an event could be stored as the latest event in a room. /// Returns a LatestEvent representing our decision. #[cfg(feature = "e2e-encryption")] -pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> { +pub fn is_suitable_for_latest_event<'a>( + event: &'a AnySyncTimelineEvent, + power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>, +) -> PossibleLatestEvent<'a> { match event { // Suitable - we have an m.room.message that was not redacted or edited AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => { @@ -114,10 +121,23 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat // We don't currently support most state events AnySyncTimelineEvent::State(state) => { - // But we make an exception for knocked state events + // But we make an exception for knocked state events *if* the current user + // can either accept or decline them if let AnySyncStateEvent::RoomMember(member) = state { if matches!(member.membership(), MembershipState::Knock) { - return PossibleLatestEvent::YesKnockedStateEvent(member); + let can_accept_or_decline_knocks = match power_levels_info { + Some((own_user_id, room_power_levels)) => { + room_power_levels.user_can_invite(own_user_id) + || room_power_levels.user_can_kick(own_user_id) + } + _ => false, + }; + + // The current user can act on the knock changes, so they should be + // displayed + if can_accept_or_decline_knocks { + return PossibleLatestEvent::YesKnockedStateEvent(member); + } } } PossibleLatestEvent::NoUnsupportedEventType @@ -345,7 +365,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); assert_eq!(m.content.msgtype.msgtype(), "m.image"); @@ -368,7 +388,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); assert_eq!(m.content.poll_start().question.text, "do you like rust?"); @@ -392,7 +412,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); } @@ -414,7 +434,7 @@ mod tests { )); assert_let!( PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) = - is_suitable_for_latest_event(&event) + is_suitable_for_latest_event(&event, None) ); } @@ -435,7 +455,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_)) ); } @@ -457,7 +477,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedMessageLikeType ); } @@ -485,7 +505,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_)) ); } @@ -507,7 +527,10 @@ mod tests { }), )); - assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted); + assert_matches!( + is_suitable_for_latest_event(&event, None), + PossibleLatestEvent::NoEncrypted + ); } #[test] @@ -524,7 +547,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedEventType ); } @@ -548,7 +571,7 @@ mod tests { )); assert_matches!( - is_suitable_for_latest_event(&event), + is_suitable_for_latest_event(&event, None), PossibleLatestEvent::NoUnsupportedMessageLikeType ); } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 3d426261f2b..cfe1625b7af 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -43,6 +43,7 @@ use ruma::{ join_rules::JoinRule, member::{MembershipState, RoomMemberEventContent}, pinned_events::RoomPinnedEventsEventContent, + power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, redaction::SyncRoomRedactionEvent, tombstone::RoomTombstoneEventContent, }, @@ -71,7 +72,7 @@ use crate::{ read_receipts::RoomReadReceipts, store::{DynStateStore, Result as StoreResult, StateStoreExt}, sync::UnreadNotificationsCount, - MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships, + Error, MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships, }; /// Indicates that a notable update of `RoomInfo` has been applied, and why. @@ -508,6 +509,17 @@ impl Room { self.inner.read().base_info.max_power_level } + /// Get the current power levels of this room. + pub async fn power_levels(&self) -> Result { + Ok(self + .store + .get_state_event_static::(self.room_id()) + .await? + .ok_or(Error::InsufficientData)? + .deserialize()? + .power_levels()) + } + /// Get the `m.room.name` of this room. /// /// The returned string may be empty if the event has been redacted, or it's diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index a6f711b0d28..f613d177b6e 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -30,7 +30,7 @@ use ruma::{ api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom}, events::{ room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent, - AnySyncStateEvent, + AnySyncStateEvent, StateEventType, }, serde::Raw, JsOption, OwnedRoomId, RoomId, UInt, UserId, @@ -698,9 +698,28 @@ async fn cache_latest_events( let mut encrypted_events = Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity()); + // Try to get room power levels from the current changes + let power_levels_from_changes = || { + let state_changes = changes?.state.get(room_info.room_id())?; + let room_power_levels_state = + state_changes.get(&StateEventType::RoomPowerLevels)?.values().next()?; + match room_power_levels_state.deserialize().ok()? { + AnySyncStateEvent::RoomPowerLevels(ev) => Some(ev.power_levels()), + _ => None, + } + }; + + // If we didn't get any info, try getting it from local data + let power_levels = match power_levels_from_changes() { + Some(power_levels) => Some(power_levels), + None => room.power_levels().await.ok(), + }; + + let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref()); + for event in events.iter().rev() { if let Ok(timeline_event) = event.raw().deserialize() { - match is_suitable_for_latest_event(&timeline_event) { + match is_suitable_for_latest_event(&timeline_event, power_levels_info) { PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) | PossibleLatestEvent::YesCallInvite(_) @@ -1740,10 +1759,23 @@ mod tests { } #[async_test] - async fn test_last_knock_member_state_event_from_sliding_sync_is_cached() { + async fn test_last_knock_event_from_sliding_sync_is_cached_if_user_has_permissions() { + let own_user_id = user_id!("@me:e.uk"); // Given a logged-in client - let client = logged_in_base_client(None).await; + let client = logged_in_base_client(Some(own_user_id)).await; let room_id = room_id!("!r:e.uk"); + + // Give the current user invite or kick permissions in this room + let power_levels = json!({ + "sender":"@alice:example.com", + "state_key":"", + "type":"m.room.power_levels", + "event_id": "$idb", + "origin_server_ts": 12344445, + "content":{ "invite": 100, "kick": 100, "users": { own_user_id: 100 } }, + "room_id": room_id, + }); + // And a knock member state event let knock_event = json!({ "sender":"@alice:example.com", @@ -1757,7 +1789,8 @@ mod tests { // When the sliding sync response contains a timeline let events = &[knock_event]; - let room = room_with_timeline(events); + let mut room = room_with_timeline(events); + room.required_state.push(Raw::new(&power_levels).unwrap().cast()); let response = response_with_room(room_id, room); client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); @@ -1770,7 +1803,49 @@ mod tests { } #[async_test] - async fn test_last_member_state_event_from_sliding_sync_is_not_cached() { + async fn test_last_knock_event_from_sliding_sync_is_not_cached_without_permissions() { + let own_user_id = user_id!("@me:e.uk"); + // Given a logged-in client + let client = logged_in_base_client(Some(own_user_id)).await; + let room_id = room_id!("!r:e.uk"); + + // Set the user as a user with no permission to invite or kick other users in + // this room + let power_levels = json!({ + "sender":"@alice:example.com", + "state_key":"", + "type":"m.room.power_levels", + "event_id": "$idb", + "origin_server_ts": 12344445, + "content":{ "invite": 50, "kick": 50, "users": { own_user_id: 0 } }, + "room_id": room_id, + }); + + // And a knock member state event + let knock_event = json!({ + "sender":"@alice:example.com", + "state_key":"@alice:example.com", + "type":"m.room.member", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content":{"membership": "knock"}, + "room_id": room_id, + }); + + // When the sliding sync response contains a timeline + let events = &[knock_event]; + let mut room = room_with_timeline(events); + room.required_state.push(Raw::new(&power_levels).unwrap().cast()); + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // Then the room doesn't hold the knock state event as the latest event + let client_room = client.get_room(room_id).expect("No room found"); + assert!(client_room.latest_event().is_none()); + } + + #[async_test] + async fn test_last_non_knock_member_state_event_from_sliding_sync_is_not_cached() { // Given a logged-in client let client = logged_in_base_client(None).await; let room_id = room_id!("!r:e.uk"); diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index d85b4d2c448..992ba6e6808 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -46,7 +46,7 @@ use ruma::{ }, name::RoomNameEventContent, pinned_events::RoomPinnedEventsEventContent, - power_levels::RoomPowerLevelsEventContent, + power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, server_acl::RoomServerAclEventContent, third_party_invite::RoomThirdPartyInviteEventContent, tombstone::RoomTombstoneEventContent, @@ -141,8 +141,9 @@ impl TimelineItemContent { /// `TimelineItemContent`. pub(crate) fn from_latest_event_content( event: AnySyncTimelineEvent, + power_levels_info: Option<(&UserId, &RoomPowerLevels)>, ) -> Option { - match is_suitable_for_latest_event(&event) { + match is_suitable_for_latest_event(&event, power_levels_info) { PossibleLatestEvent::YesRoomMessage(m) => { Some(Self::from_suitable_latest_event_content(m)) } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index c94c858cdf7..b91dbb72abb 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -158,9 +158,18 @@ impl EventTimelineItem { let event_id = event.event_id().to_owned(); let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false); + // Get the room's power levels for calculating the latest event + let power_levels = if let Some(room) = client.get_room(room_id) { + room.power_levels().await.ok() + } else { + None + }; + let room_power_levels_info = client.user_id().zip(power_levels.as_ref()); + // If we don't (yet) know how to handle this type of message, return `None` // here. If we do, convert it into a `TimelineItemContent`. - let content = TimelineItemContent::from_latest_event_content(event)?; + let content = + TimelineItemContent::from_latest_event_content(event, room_power_levels_info)?; // We don't currently bundle any reactions with the main event. This could // conceivably be wanted in the message preview in future. @@ -780,6 +789,27 @@ mod tests { ); let client = logged_in_client(None).await; + // Add power levels state event, otherwise the knock state event can't be used + // as the latest event + let power_level_event = sync_state_event!({ + "type": "m.room.power_levels", + "content": {}, + "event_id": "$143278582443PhrSn:example.org", + "origin_server_ts": 143273581, + "room_id": room_id, + "sender": user_id, + "state_key": "", + "unsigned": { + "age": 1234 + } + }); + let mut room = http::response::Room::new(); + room.required_state.push(power_level_event); + + // And the room is stored in the client so it can be extracted when needed + let response = response_with_room(room_id, room); + client.process_sliding_sync_test_helper(&response).await.unwrap(); + // When we construct a timeline event from it let event = SyncTimelineEvent::new(raw_event.cast()); let timeline_item = @@ -1040,7 +1070,6 @@ mod tests { "room_id": room_id, "sender": user_id, "state_key": user_id, - "type": "m.room.member", "unsigned": { "age": 1234 } diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 5561deee315..d6a4624ecff 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2139,7 +2139,7 @@ impl Room { &self, updates: Vec<(&UserId, Int)>, ) -> Result { - let mut power_levels = self.room_power_levels().await?; + let mut power_levels = self.power_levels().await?; for (user_id, new_level) in updates { if new_level == power_levels.users_default { @@ -2157,7 +2157,7 @@ impl Room { /// Any values that are `None` in the given `RoomPowerLevelChanges` will /// remain unchanged. pub async fn apply_power_level_changes(&self, changes: RoomPowerLevelChanges) -> Result<()> { - let mut power_levels = self.room_power_levels().await?; + let mut power_levels = self.power_levels().await?; power_levels.apply(changes)?; self.send_state_event(RoomPowerLevelsEventContent::from(power_levels)).await?; Ok(()) @@ -2180,7 +2180,7 @@ impl Room { let default_power_levels = RoomPowerLevels::from(RoomPowerLevelsEventContent::new()); let changes = RoomPowerLevelChanges::from(default_power_levels); self.apply_power_level_changes(changes).await?; - self.room_power_levels().await + Ok(self.power_levels().await?) } /// Gets the suggested role for the user with the provided `user_id`. @@ -2197,14 +2197,14 @@ impl Room { /// This method checks the `RoomPowerLevels` events instead of loading the /// member list and looking for the member. pub async fn get_user_power_level(&self, user_id: &UserId) -> Result { - let event = self.room_power_levels().await?; + let event = self.power_levels().await?; Ok(event.for_user(user_id).into()) } /// Gets a map with the `UserId` of users with power levels other than `0` /// and this power level. pub async fn users_with_power_levels(&self) -> HashMap { - let power_levels = self.room_power_levels().await.ok(); + let power_levels = self.power_levels().await.ok(); let mut user_power_levels = HashMap::::new(); if let Some(power_levels) = power_levels { for (id, level) in power_levels.users.into_iter() { @@ -2487,7 +2487,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_redact_own(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_redact_own_event(user_id)) + Ok(self.power_levels().await?.user_can_redact_own_event(user_id)) } /// Returns true if the user with the given user_id is able to redact @@ -2495,7 +2495,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_redact_other(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_redact_event_of_other(user_id)) + Ok(self.power_levels().await?.user_can_redact_event_of_other(user_id)) } /// Returns true if the user with the given user_id is able to ban in the @@ -2503,7 +2503,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_ban(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_ban(user_id)) + Ok(self.power_levels().await?.user_can_ban(user_id)) } /// Returns true if the user with the given user_id is able to kick in the @@ -2511,7 +2511,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_invite(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_invite(user_id)) + Ok(self.power_levels().await?.user_can_invite(user_id)) } /// Returns true if the user with the given user_id is able to kick in the @@ -2519,7 +2519,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_kick(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_kick(user_id)) + Ok(self.power_levels().await?.user_can_kick(user_id)) } /// Returns true if the user with the given user_id is able to send a @@ -2531,7 +2531,7 @@ impl Room { user_id: &UserId, state_event: StateEventType, ) -> Result { - Ok(self.room_power_levels().await?.user_can_send_state(user_id, state_event)) + Ok(self.power_levels().await?.user_can_send_state(user_id, state_event)) } /// Returns true if the user with the given user_id is able to send a @@ -2543,7 +2543,7 @@ impl Room { user_id: &UserId, message: MessageLikeEventType, ) -> Result { - Ok(self.room_power_levels().await?.user_can_send_message(user_id, message)) + Ok(self.power_levels().await?.user_can_send_message(user_id, message)) } /// Returns true if the user with the given user_id is able to pin or unpin @@ -2552,7 +2552,7 @@ impl Room { /// The call may fail if there is an error in getting the power levels. pub async fn can_user_pin_unpin(&self, user_id: &UserId) -> Result { Ok(self - .room_power_levels() + .power_levels() .await? .user_can_send_state(user_id, StateEventType::RoomPinnedEvents)) } @@ -2562,7 +2562,7 @@ impl Room { /// /// The call may fail if there is an error in getting the power levels. pub async fn can_user_trigger_room_notification(&self, user_id: &UserId) -> Result { - Ok(self.room_power_levels().await?.user_can_trigger_room_notification(user_id)) + Ok(self.power_levels().await?.user_can_trigger_room_notification(user_id)) } /// Get a list of servers that should know this room. diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 497df17e03c..e8f294bb0d0 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -624,7 +624,7 @@ async fn test_reset_power_levels() { .mount(&server) .await; - let initial_power_levels = room.room_power_levels().await.unwrap(); + let initial_power_levels = room.power_levels().await.unwrap(); assert_eq!(initial_power_levels.events[&TimelineEventType::RoomAvatar], int!(100)); room.reset_power_levels().await.unwrap(); From 61f48bac6611beed80dedc36b2d68bdebbb31eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 29 Oct 2024 12:38:47 +0100 Subject: [PATCH 3/3] refactor(room): remove `sdk::Room::room_power_levels` function This has been replaced by `sdk_base::Room::power_levels`, which can also be used from `sdk::Room` --- crates/matrix-sdk/src/room/mod.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index d6a4624ecff..420aaf8eb5d 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2163,16 +2163,6 @@ impl Room { Ok(()) } - /// Get the current power levels of this room. - pub async fn room_power_levels(&self) -> Result { - Ok(self - .get_state_event_static::() - .await? - .ok_or(Error::InsufficientData)? - .deserialize()? - .power_levels()) - } - /// Resets the room's power levels to the default values /// /// [spec]: https://spec.matrix.org/v1.9/client-server-api/#mroompower_levels