diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 503766f97..59f368496 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -497,11 +497,12 @@ impl BaseClient { ); if let Some(room) = changes.room_infos.get_mut(room_id) { - room.base_info.dm_target = Some(user_id.clone()); + room.base_info.dm_targets.insert(user_id.clone()); } else if let Some(room) = self.store.get_room(room_id) { let mut info = room.clone_info(); - info.base_info.dm_target = Some(user_id.clone()); - changes.add_room(info); + if info.base_info.dm_targets.insert(user_id.clone()) { + changes.add_room(info); + } } } } diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index ca241bf76..bd2f0e85c 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -1,7 +1,7 @@ mod members; mod normal; -use std::cmp::max; +use std::{cmp::max, collections::HashSet}; pub use members::RoomMember; pub use normal::{Room, RoomInfo, RoomType}; @@ -29,9 +29,9 @@ pub struct BaseRoomInfo { pub(crate) canonical_alias: Option, /// The `m.room.create` event content of this room. pub(crate) create: Option, - /// The user id this room is sharing the direct message with, if the room is - /// a direct message. - pub(crate) dm_target: Option, + /// A list of user ids this room is considered as direct message, if this + /// room is a DM. + pub(crate) dm_targets: HashSet, /// The `m.room.encryption` event content that enabled E2EE in this room. pub(crate) encryption: Option, /// The guest access policy of this room. @@ -183,7 +183,7 @@ impl Default for BaseRoomInfo { avatar_url: None, canonical_alias: None, create: None, - dm_target: None, + dm_targets: Default::default(), encryption: None, guest_access: GuestAccess::Forbidden, history_visibility: HistoryVisibility::WorldReadable, diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 969afed7b..53a5cee5f 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::{Arc, RwLock as SyncRwLock}; +use std::{ + collections::HashSet, + sync::{Arc, RwLock as SyncRwLock}, +}; use dashmap::DashSet; use futures_channel::mpsc; @@ -184,17 +187,17 @@ impl Room { /// Is this room considered a direct message. pub fn is_direct(&self) -> bool { - self.inner.read().unwrap().base_info.dm_target.is_some() + !self.inner.read().unwrap().base_info.dm_targets.is_empty() } - /// If this room is a direct message, get the member that we're sharing the + /// If this room is a direct message, get the members that we're sharing the /// room with. /// /// *Note*: The member list might have been modified in the meantime and - /// the target might not even be in the room anymore. This setting should + /// the targets might not even be in the room anymore. This setting should /// only be considered as guidance. - pub fn direct_target(&self) -> Option { - self.inner.read().unwrap().base_info.dm_target.clone() + pub fn direct_targets(&self) -> HashSet { + self.inner.read().unwrap().base_info.dm_targets.clone() } /// Is the room encrypted. diff --git a/crates/matrix-sdk/src/encryption/identities/users.rs b/crates/matrix-sdk/src/encryption/identities/users.rs index cf06ad262..2886534a7 100644 --- a/crates/matrix-sdk/src/encryption/identities/users.rs +++ b/crates/matrix-sdk/src/encryption/identities/users.rs @@ -465,6 +465,15 @@ impl OtherUserIdentity { let content = self.inner.verification_request_content(methods.clone()).await; let room = if let Some(room) = self.direct_message_room.read().await.as_ref() { + // Make sure that the user, to be verified, is still in the room + if !room + .active_members() + .await? + .iter() + .any(|member| member.user_id() == self.inner.user_id()) + { + room.invite_user_by_id(self.inner.user_id()).await?; + } room.clone() } else if let Some(room) = self.client.create_dm_room(self.inner.user_id().to_owned()).await? diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index f32195580..fa75cc35b 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -404,11 +404,12 @@ impl Client { #[cfg(feature = "encryption")] fn get_dm_room(&self, user_id: &UserId) -> Option { let rooms = self.joined_rooms(); - let room_pairs: Vec<_> = - rooms.iter().map(|r| (r.room_id().to_owned(), r.direct_target())).collect(); - trace!(rooms = ?room_pairs, "Finding direct room"); - let room = rooms.into_iter().find(|r| r.direct_target().as_deref() == Some(user_id)); + // Find the room we share with the `user_id` and only with `user_id` + let room = rooms.into_iter().find(|r| { + let targets = r.direct_targets(); + targets.len() == 1 && targets.contains(user_id) + }); trace!(?room, "Found room"); room diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index 99f631f35..2820907ae 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, sync::Arc}; +use std::{collections::BTreeMap, ops::Deref, sync::Arc}; use futures_core::stream::Stream; use matrix_sdk_base::{ @@ -8,6 +8,7 @@ use matrix_sdk_base::{ use matrix_sdk_common::locks::Mutex; use ruma::{ api::client::{ + config::set_global_account_data, filter::{LazyLoadOptions, RoomEventFilter}, membership::{get_member_events, join_room_by_id, leave_room}, message::get_message_events::{self, v3::Direction}, @@ -16,10 +17,11 @@ use ruma::{ }, assign, events::{ + direct::DirectEvent, room::{history_visibility::HistoryVisibility, MediaSource}, tag::{TagInfo, TagName}, - AnyRoomAccountDataEvent, AnyStateEvent, AnySyncStateEvent, RedactContent, - RedactedEventContent, RoomAccountDataEvent, RoomAccountDataEventContent, + AnyRoomAccountDataEvent, AnyStateEvent, AnySyncStateEvent, GlobalAccountDataEventType, + RedactContent, RedactedEventContent, RoomAccountDataEvent, RoomAccountDataEventContent, RoomAccountDataEventType, StateEventContent, StateEventType, StaticEventContent, SyncStateEvent, }, @@ -30,7 +32,7 @@ use ruma::{ use crate::{ media::{MediaFormat, MediaRequest}, room::RoomType, - BaseRoom, Client, HttpError, HttpResult, Result, RoomMember, + BaseRoom, Client, Error, HttpError, HttpResult, Result, RoomMember, }; /// A struct containing methods that are common for Joined, Invited and Left @@ -817,6 +819,56 @@ impl Common { let request = delete_tag::v3::Request::new(&user_id, self.inner.room_id(), tag.as_ref()); self.client.send(request, None).await } + + /// Sets whether this room is a DM. + /// + /// When setting this room as DM, it will be marked as DM for all active + /// members of the room. When unsetting this room as DM, it will be + /// unmarked as DM for all users, not just the members. + /// + /// # Arguments + /// * `is_direct` - Whether to mark this room as direct. + pub async fn set_is_direct(&self, is_direct: bool) -> Result<()> { + let user_id = self + .client + .user_id() + .await + .ok_or_else(|| Error::from(HttpError::AuthenticationRequired))?; + + let mut content = self + .client + .store() + .get_account_data_event(GlobalAccountDataEventType::Direct) + .await? + .map(|e| e.deserialize_as::()) + .transpose()? + .map(|e| e.content) + .unwrap_or_else(|| ruma::events::direct::DirectEventContent(BTreeMap::new())); + + let this_room_id = self.inner.room_id(); + + if is_direct { + let room_members = self.active_members().await?; + for member in room_members { + let entry = content.entry(member.user_id().to_owned()).or_default(); + if !entry.iter().any(|room_id| room_id == this_room_id) { + entry.push(this_room_id.to_owned()); + } + } + } else { + for (_, list) in content.iter_mut() { + list.retain(|room_id| *room_id != this_room_id); + } + + // Remove user ids that don't have any room marked as DM + content.retain(|_, list| !list.is_empty()); + } + + let request = set_global_account_data::v3::Request::new(&content, &user_id)?; + + self.client.send(request, None).await?; + Ok(()) + } } /// Options for [`messages`][Common::messages].