diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 5cf1fd4e6..b891e6234 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -271,7 +271,9 @@ impl Room { /// /// *Note*: The member list might have been modified in the meantime and /// the targets might not even be in the room anymore. This setting should - /// only be considered as guidance. + /// only be considered as guidance. We leave members in this list to allow + /// us to re-find a DM with a user even if they have left, since we may + /// want to re-invite them. pub fn direct_targets(&self) -> HashSet { self.inner.read().unwrap().base_info.dm_targets.clone() } diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 183d7b30f..f0dce153d 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -387,20 +387,23 @@ fn process_room_properties(room_data: &v4::SlidingSyncRoom, room_info: &mut Room #[cfg(test)] mod test { + use std::collections::{BTreeMap, HashSet}; + use matrix_sdk_test::async_test; use ruma::{ device_id, event_id, events::{ + direct::DirectEventContent, room::{ avatar::RoomAvatarEventContent, canonical_alias::RoomCanonicalAliasEventContent, member::{MembershipState, RoomMemberEventContent}, }, - AnySyncStateEvent, StateEventContent, + AnySyncStateEvent, GlobalAccountDataEventContent, StateEventContent, }, mxc_uri, room_alias_id, room_id, serde::Raw, - uint, user_id, MxcUri, RoomAliasId, RoomId, UserId, + uint, user_id, MxcUri, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId, }; use serde_json::json; @@ -510,6 +513,55 @@ mod test { assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited); } + #[async_test] + async fn other_person_leaving_a_dm_is_reflected_in_their_membership_and_direct_targets() { + let room_id = room_id!("!r:e.uk"); + let user_a_id = user_id!("@a:e.uk"); + let user_b_id = user_id!("@b:e.uk"); + + // Given we have a DM with B, who is joined + let client = logged_in_client().await; + create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await; + + // (Sanity: B is a direct target, and is in Join state) + assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join); + + // When B leaves + update_room_membership(&client, room_id, user_b_id, MembershipState::Leave).await; + + // Then B is still a direct target, and is in Leave state (B is a direct target + // because we want to return to our old DM in the UI even if the other + // user left, so we can reinvite them. See https://github.com/matrix-org/matrix-rust-sdk/issues/2017) + assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave); + } + + #[async_test] + async fn other_person_refusing_invite_to_a_dm_is_reflected_in_their_membership_and_direct_targets( + ) { + let room_id = room_id!("!r:e.uk"); + let user_a_id = user_id!("@a:e.uk"); + let user_b_id = user_id!("@b:e.uk"); + + // Given I have invited B to a DM + let client = logged_in_client().await; + create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await; + + // (Sanity: B is a direct target, and is in Invite state) + assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite); + + // When B declines the invitation (i.e. leaves) + update_room_membership(&client, room_id, user_b_id, MembershipState::Leave).await; + + // Then B is still a direct target, and is in Leave state (B is a direct target + // because we want to return to our old DM in the UI even if the other + // user left, so we can reinvite them. See https://github.com/matrix-org/matrix-rust-sdk/issues/2017) + assert!(direct_targets(&client, room_id).contains(user_b_id)); + assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave); + } + #[async_test] async fn avatar_is_found_when_processing_sliding_sync_response() { // Given a logged-in client @@ -594,6 +646,65 @@ mod test { assert_eq!(client_room.canonical_alias(), Some(room_alias_id.to_owned())); } + async fn membership( + client: &BaseClient, + room_id: &RoomId, + user_id: &UserId, + ) -> MembershipState { + let room = client.get_room(room_id).expect("Room not found!"); + let member = room.get_member(user_id).await.unwrap().expect("B not in room"); + member.membership().clone() + } + + fn direct_targets(client: &BaseClient, room_id: &RoomId) -> HashSet { + let room = client.get_room(room_id).expect("Room not found!"); + room.direct_targets() + } + + /// Create a DM with the other user, setting our membership to Join and + /// theirs to other_state + async fn create_dm( + client: &BaseClient, + room_id: &RoomId, + my_id: &UserId, + their_id: &UserId, + other_state: MembershipState, + ) { + let mut room = v4::SlidingSyncRoom::new(); + set_room_joined(&mut room, my_id); + room.required_state.push(make_membership_event(their_id, other_state)); + let mut response = response_with_room(room_id, room).await; + set_direct_with(&mut response, their_id.to_owned(), vec![room_id.to_owned()]); + client.process_sliding_sync(&response).await.expect("Failed to process sync"); + } + + /// Set this user's membership within this room to new_state + async fn update_room_membership( + client: &BaseClient, + room_id: &RoomId, + user_id: &UserId, + new_state: MembershipState, + ) { + let mut room = v4::SlidingSyncRoom::new(); + room.required_state.push(make_membership_event(user_id, new_state)); + let response = response_with_room(room_id, room).await; + client.process_sliding_sync(&response).await.expect("Failed to process sync"); + } + + fn set_direct_with( + response: &mut v4::Response, + user_id: OwnedUserId, + room_ids: Vec, + ) { + let mut direct_content = BTreeMap::new(); + direct_content.insert(user_id, room_ids); + response + .extensions + .account_data + .global + .push(make_global_account_data_event(DirectEventContent(direct_content))); + } + async fn logged_in_client() -> BaseClient { let client = BaseClient::new(); client @@ -671,6 +782,15 @@ mod test { make_state_event(user_id, user_id.as_str(), RoomMemberEventContent::new(state), None) } + fn make_global_account_data_event(content: C) -> Raw { + Raw::new(&json!({ + "type": content.event_type(), + "content": content, + })) + .expect("Failed to create account data event") + .cast() + } + fn make_state_event( sender: &UserId, state_key: &str,