matrix_sdk_ffi/
room_preview.rs

1use anyhow::Context as _;
2use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
3use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
4use tracing::warn;
5
6use crate::{
7    client::JoinRule,
8    error::ClientError,
9    room::{Membership, RoomHero},
10    room_member::RoomMember,
11    utils::AsyncRuntimeDropped,
12};
13
14/// A room preview for a room. It's intended to be used to represent rooms that
15/// aren't joined yet.
16#[derive(uniffi::Object)]
17pub struct RoomPreview {
18    inner: SdkRoomPreview,
19    client: AsyncRuntimeDropped<Client>,
20}
21
22#[matrix_sdk_ffi_macros::export]
23impl RoomPreview {
24    /// Returns the room info the preview contains.
25    pub fn info(&self) -> Result<RoomPreviewInfo, ClientError> {
26        let info = &self.inner;
27        Ok(RoomPreviewInfo {
28            room_id: info.room_id.to_string(),
29            canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
30            name: info.name.clone(),
31            topic: info.topic.clone(),
32            avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
33            num_joined_members: info.num_joined_members,
34            num_active_members: info.num_active_members,
35            room_type: info.room_type.as_ref().into(),
36            is_history_world_readable: info.is_world_readable,
37            membership: info.state.map(|state| state.into()),
38            join_rule: info
39                .join_rule
40                .clone()
41                .try_into()
42                .map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
43            is_direct: info.is_direct,
44            heroes: info
45                .heroes
46                .as_ref()
47                .map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
48        })
49    }
50
51    /// Leave the room if the room preview state is either joined, invited or
52    /// knocked.
53    ///
54    /// If rejecting an invite then also forget it as an extra layer of
55    /// protection against spam attacks.
56    ///
57    /// Will return an error otherwise.
58    pub async fn leave(&self) -> Result<(), ClientError> {
59        let room =
60            self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
61
62        let should_forget = matches!(room.state(), matrix_sdk::RoomState::Invited);
63
64        room.leave().await.map_err(ClientError::from)?;
65
66        if should_forget {
67            _ = self.forget().await;
68        }
69
70        Ok(())
71    }
72
73    /// Get the user who created the invite, if any.
74    pub async fn inviter(&self) -> Option<RoomMember> {
75        let room = self.client.get_room(&self.inner.room_id)?;
76        let invite_details = room.invite_details().await.ok()?;
77        invite_details.inviter.and_then(|m| m.try_into().ok())
78    }
79
80    /// Forget the room if we had access to it, and it was left or banned.
81    pub async fn forget(&self) -> Result<(), ClientError> {
82        let room =
83            self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
84        room.forget().await?;
85        Ok(())
86    }
87
88    /// Get the membership details for the current user.
89    pub async fn own_membership_details(&self) -> Option<RoomMembershipDetails> {
90        let room = self.client.get_room(&self.inner.room_id)?;
91
92        let (own_member, sender_member) = match room.own_membership_details().await {
93            Ok(memberships) => memberships,
94            Err(error) => {
95                warn!("Couldn't get membership info: {error}");
96                return None;
97            }
98        };
99
100        Some(RoomMembershipDetails {
101            own_room_member: own_member.try_into().ok()?,
102            sender_room_member: sender_member.and_then(|member| member.try_into().ok()),
103        })
104    }
105}
106
107/// Contains the current user's room member info and the optional room member
108/// info of the sender of the `m.room.member` event that this info represents.
109#[derive(uniffi::Record)]
110pub struct RoomMembershipDetails {
111    pub own_room_member: RoomMember,
112    pub sender_room_member: Option<RoomMember>,
113}
114
115impl RoomPreview {
116    pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
117        Self { client, inner }
118    }
119}
120
121/// The preview of a room, be it invited/joined/left, or not.
122#[derive(uniffi::Record)]
123pub struct RoomPreviewInfo {
124    /// The room id for this room.
125    pub room_id: String,
126    /// The canonical alias for the room.
127    pub canonical_alias: Option<String>,
128    /// The room's name, if set.
129    pub name: Option<String>,
130    /// The room's topic, if set.
131    pub topic: Option<String>,
132    /// The MXC URI to the room's avatar, if set.
133    pub avatar_url: Option<String>,
134    /// The number of joined members.
135    pub num_joined_members: u64,
136    /// The number of active members, if known (joined + invited).
137    pub num_active_members: Option<u64>,
138    /// The room type (space, custom) or nothing, if it's a regular room.
139    pub room_type: RoomType,
140    /// Is the history world-readable for this room?
141    pub is_history_world_readable: Option<bool>,
142    /// The membership state for the current user, if known.
143    pub membership: Option<Membership>,
144    /// The join rule for this room (private, public, knock, etc.).
145    pub join_rule: JoinRule,
146    /// Whether the room is direct or not, if known.
147    pub is_direct: Option<bool>,
148    /// Room heroes.
149    pub heroes: Option<Vec<RoomHero>>,
150}
151
152impl TryFrom<SpaceRoomJoinRule> for JoinRule {
153    type Error = ();
154
155    fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
156        Ok(match join_rule {
157            SpaceRoomJoinRule::Invite => JoinRule::Invite,
158            SpaceRoomJoinRule::Knock => JoinRule::Knock,
159            SpaceRoomJoinRule::Private => JoinRule::Private,
160            SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() },
161            SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() },
162            SpaceRoomJoinRule::Public => JoinRule::Public,
163            SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() },
164            _ => {
165                warn!("unhandled SpaceRoomJoinRule: {join_rule}");
166                return Err(());
167            }
168        })
169    }
170}
171
172/// The type of room for a [`RoomPreviewInfo`].
173#[derive(Debug, Clone, uniffi::Enum)]
174pub enum RoomType {
175    /// It's a plain chat room.
176    Room,
177    /// It's a space that can group several rooms.
178    Space,
179    /// It's a custom implementation.
180    Custom { value: String },
181}
182
183impl From<Option<&RumaRoomType>> for RoomType {
184    fn from(value: Option<&RumaRoomType>) -> Self {
185        match value {
186            Some(RumaRoomType::Space) => RoomType::Space,
187            Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
188                // SAFETY: this was checked in the match branch above
189                value: value.unwrap().to_string(),
190            },
191            _ => RoomType::Room,
192        }
193    }
194}