Skip to content

Commit

Permalink
chore(room_preview): add RoomListItem::preview_room (#4152)
Browse files Browse the repository at this point in the history
This method will return a `RoomPreview` for the provided room id. 

Also added `fn RoomPreview::leave()` action to be able to decline
invites or cancel knocks, since there wasn't a
`Client::leave_room_by_id` counterpart as there is for join.

The PR also deprecates `RoomListItem::invited_room`, since we have a
better alternative now.

Co-authored-by: Benjamin Bouvier <[email protected]>
  • Loading branch information
jmartinesp and bnjbvr authored Oct 25, 2024
1 parent d3d7c03 commit 40f4fc1
Show file tree
Hide file tree
Showing 14 changed files with 411 additions and 64 deletions.
23 changes: 15 additions & 8 deletions bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,7 @@ impl Client {
&self,
room_id: String,
via_servers: Vec<String>,
) -> Result<RoomPreview, ClientError> {
) -> Result<Arc<RoomPreview>, ClientError> {
let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;

let via_servers = via_servers
Expand All @@ -1040,26 +1040,26 @@ impl Client {
// rustc win that one fight.
let room_id: &RoomId = &room_id;

let sdk_room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
let room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;

Ok(RoomPreview::from_sdk(sdk_room_preview))
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
}

/// Given a room alias, get the preview of a room, to interact with it.
pub async fn get_room_preview_from_room_alias(
&self,
room_alias: String,
) -> Result<RoomPreview, ClientError> {
) -> Result<Arc<RoomPreview>, ClientError> {
let room_alias =
RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;

// The `into()` call below doesn't work if I do `(&room_id).into()`, so I let
// rustc win that one fight.
let room_alias: &RoomAliasId = &room_alias;

let sdk_room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
let room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;

Ok(RoomPreview::from_sdk(sdk_room_preview))
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
}

/// Waits until an at least partially synced room is received, and returns
Expand Down Expand Up @@ -1804,7 +1804,7 @@ impl From<OidcPrompt> for SdkOidcPrompt {
}

/// The rule used for users wishing to join this room.
#[derive(uniffi::Enum)]
#[derive(Debug, Clone, uniffi::Enum)]
pub enum JoinRule {
/// Anyone can join the room without any prior action.
Public,
Expand All @@ -1830,10 +1830,16 @@ pub enum JoinRule {
/// conditions described in a set of [`AllowRule`]s, or they can request
/// an invite to the room.
KnockRestricted { rules: Vec<AllowRule> },

/// A custom join rule, up for interpretation by the consumer.
Custom {
/// The string representation for this custom rule.
repr: String,
},
}

/// An allow rule which defines a condition that allows joining a room.
#[derive(uniffi::Enum)]
#[derive(Debug, Clone, uniffi::Enum)]
pub enum AllowRule {
/// Only a member of the `room_id` Room can join the one this rule is used
/// in.
Expand All @@ -1857,6 +1863,7 @@ impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
let rules = allow_rules_from(rules)?;
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
}
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions bindings/matrix-sdk-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use matrix_sdk::{
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use uniffi::UnexpectedUniFFICallbackError;

use crate::room_list::RoomListError;

#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("client error: {msg}")]
Expand Down Expand Up @@ -128,6 +131,12 @@ impl From<RoomError> for ClientError {
}
}

impl From<RoomListError> for ClientError {
fn from(e: RoomListError) -> Self {
Self::new(e)
}
}

impl From<EventCacheError> for ClientError {
fn from(e: EventCacheError) -> Self {
Self::new(e)
Expand Down
2 changes: 1 addition & 1 deletion bindings/matrix-sdk-ffi/src/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use crate::{
TaskHandle,
};

#[derive(Debug, uniffi::Enum)]
#[derive(Debug, Clone, uniffi::Enum)]
pub enum Membership {
Invited,
Joined,
Expand Down
63 changes: 55 additions & 8 deletions bindings/matrix-sdk-ffi/src/room_list.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
#![allow(deprecated)]

use std::{
fmt::Debug,
mem::{ManuallyDrop, MaybeUninit},
ptr::addr_of_mut,
sync::Arc,
time::Duration,
};

use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt, TryFutureExt};
Expand All @@ -16,12 +24,14 @@ use matrix_sdk_ui::{
timeline::default_event_filter,
unable_to_decrypt_hook::UtdHookManager,
};
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
use tokio::sync::RwLock;

use crate::{
error::ClientError,
room::{Membership, Room},
room_info::RoomInfo,
room_preview::RoomPreview,
timeline::{EventTimelineItem, Timeline},
timeline_event_filter::TimelineEventTypeFilter,
TaskHandle, RUNTIME,
Expand All @@ -48,7 +58,7 @@ pub enum RoomListError {
#[error("Event cache ran into an error: {error}")]
EventCache { error: String },
#[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")]
IncorrectRoomMembership { expected: Membership, actual: Membership },
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
}

impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
Expand Down Expand Up @@ -574,31 +584,68 @@ impl RoomListItem {
}

/// Builds a `Room` FFI from an invited room without initializing its
/// internal timeline
/// internal timeline.
///
/// An error will be returned if the room is a state different than invited
/// An error will be returned if the room is a state different than invited.
///
/// ⚠️ Holding on to this room instance after it has been joined is not
/// safe. Use `full_room` instead
/// safe. Use `full_room` instead.
#[deprecated(note = "Please use `preview_room` instead.")]
fn invited_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Invited) {
return Err(RoomListError::IncorrectRoomMembership {
expected: Membership::Invited,
expected: vec![Membership::Invited],
actual: self.membership(),
});
}

Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
}

/// Builds a `RoomPreview` from a room list item. This is intended for
/// invited or knocked rooms.
///
/// An error will be returned if the room is in a state other than invited
/// or knocked.
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
// Validate parameters first.
let server_names: Vec<OwnedServerName> = via
.into_iter()
.map(|server| ServerName::parse(server).map_err(ClientError::from))
.collect::<Result<_, ClientError>>()?;

// Validate internal room state.
let membership = self.membership();
if !matches!(membership, Membership::Invited | Membership::Knocked) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Invited, Membership::Knocked],
actual: membership,
}
.into());
}

// Do the thing.
let client = self.inner.client();
let (room_or_alias_id, server_names) = if let Some(alias) = self.inner.canonical_alias() {
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
(room_or_alias_id, Vec::new())
} else {
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
(room_or_alias_id, server_names)
};

let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;

Ok(Arc::new(RoomPreview::new(ManuallyDrop::new(client), room_preview)))
}

/// Build a full `Room` FFI object, filling its associated timeline.
///
/// An error will be returned if the room is a state different than joined
/// or if its internal timeline hasn't been initialized.
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Joined) {
return Err(RoomListError::IncorrectRoomMembership {
expected: Membership::Joined,
expected: vec![Membership::Joined],
actual: self.membership(),
});
}
Expand Down
121 changes: 92 additions & 29 deletions bindings/matrix-sdk-ffi/src/room_preview.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,78 @@
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState};
use std::mem::ManuallyDrop;

use anyhow::Context as _;
use async_compat::TOKIO1 as RUNTIME;
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
use ruma::space::SpaceRoomJoinRule;
use tracing::warn;

use crate::{client::JoinRule, error::ClientError, room::Membership};

/// A room preview for a room. It's intended to be used to represent rooms that
/// aren't joined yet.
#[derive(uniffi::Object)]
pub struct RoomPreview {
inner: SdkRoomPreview,
client: ManuallyDrop<Client>,
}

impl Drop for RoomPreview {
fn drop(&mut self) {
// Dropping the inner OlmMachine must happen within a tokio context
// because deadpool drops sqlite connections in the DB pool on tokio's
// blocking threadpool to avoid blocking async worker threads.
let _guard = RUNTIME.enter();
// SAFETY: self.client is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.client);
}
}
}

#[matrix_sdk_ffi_macros::export]
impl RoomPreview {
/// Returns the room info the preview contains.
pub fn info(&self) -> Result<RoomPreviewInfo, ClientError> {
let info = &self.inner;
Ok(RoomPreviewInfo {
room_id: info.room_id.to_string(),
canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
name: info.name.clone(),
topic: info.topic.clone(),
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
num_joined_members: info.num_joined_members,
room_type: info.room_type.as_ref().map(|room_type| room_type.to_string()),
is_history_world_readable: info.is_world_readable,
membership: info.state.map(|state| state.into()),
join_rule: info
.join_rule
.clone()
.try_into()
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
})
}

/// Leave the room if the room preview state is either joined, invited or
/// knocked.
///
/// Will return an error otherwise.
pub async fn leave(&self) -> Result<(), ClientError> {
let room =
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
room.leave().await.map_err(Into::into)
}
}

impl RoomPreview {
pub(crate) fn new(client: ManuallyDrop<Client>, inner: SdkRoomPreview) -> Self {
Self { client, inner }
}
}

/// The preview of a room, be it invited/joined/left, or not.
#[derive(uniffi::Record)]
pub struct RoomPreview {
pub struct RoomPreviewInfo {
/// The room id for this room.
pub room_id: String,
/// The canonical alias for the room.
Expand All @@ -20,34 +89,28 @@ pub struct RoomPreview {
pub room_type: Option<String>,
/// Is the history world-readable for this room?
pub is_history_world_readable: bool,
/// Is the room joined by the current user?
pub is_joined: bool,
/// Is the current user invited to this room?
pub is_invited: bool,
/// is the join rule public for this room?
pub is_public: bool,
/// Can we knock (or restricted-knock) to this room?
pub can_knock: bool,
/// The membership state for the current user, if known.
pub membership: Option<Membership>,
/// The join rule for this room (private, public, knock, etc.).
pub join_rule: JoinRule,
}

impl RoomPreview {
pub(crate) fn from_sdk(preview: SdkRoomPreview) -> Self {
Self {
room_id: preview.room_id.to_string(),
canonical_alias: preview.canonical_alias.map(|alias| alias.to_string()),
name: preview.name,
topic: preview.topic,
avatar_url: preview.avatar_url.map(|url| url.to_string()),
num_joined_members: preview.num_joined_members,
room_type: preview.room_type.map(|room_type| room_type.to_string()),
is_history_world_readable: preview.is_world_readable,
is_joined: preview.state.map_or(false, |state| state == RoomState::Joined),
is_invited: preview.state.map_or(false, |state| state == RoomState::Invited),
is_public: preview.join_rule == SpaceRoomJoinRule::Public,
can_knock: matches!(
preview.join_rule,
SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Knock
),
}
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
type Error = ();

fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
Ok(match join_rule {
SpaceRoomJoinRule::Invite => JoinRule::Invite,
SpaceRoomJoinRule::Knock => JoinRule::Knock,
SpaceRoomJoinRule::Private => JoinRule::Private,
SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() },
SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() },
SpaceRoomJoinRule::Public => JoinRule::Public,
SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() },
_ => {
warn!("unhandled SpaceRoomJoinRule: {join_rule}");
return Err(());
}
})
}
}
6 changes: 3 additions & 3 deletions crates/matrix-sdk-base/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use std::{collections::BTreeMap, fmt};
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent};
use ruma::{
api::client::sync::sync_events::{
v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom},
v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate},
UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
events::{
Expand Down Expand Up @@ -78,7 +78,7 @@ pub struct RoomUpdates {
/// The rooms that the user has been invited to.
pub invite: BTreeMap<OwnedRoomId, InvitedRoomUpdate>,
/// The rooms that the user has knocked on.
pub knocked: BTreeMap<OwnedRoomId, KnockedRoom>,
pub knocked: BTreeMap<OwnedRoomId, KnockedRoomUpdate>,
}

impl RoomUpdates {
Expand Down Expand Up @@ -254,7 +254,7 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
}
}

struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoom>);
struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoomUpdate>);

#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> {
Expand Down
2 changes: 1 addition & 1 deletion crates/matrix-sdk/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,7 @@ impl Client {
};

if let Some(room) = self.get_room(&room_id) {
return Ok(RoomPreview::from_known(&room));
return Ok(RoomPreview::from_known(&room).await);
}

RoomPreview::from_unknown(self, room_id, room_or_alias_id, via).await
Expand Down
Loading

0 comments on commit 40f4fc1

Please sign in to comment.