Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(room): add RequestToJoinRoom subscriptions #4338

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion bindings/matrix-sdk-ffi/src/room.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{collections::HashMap, pin::pin, sync::Arc};

use anyhow::{Context, Result};
use futures_util::StreamExt;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{
crypto::LocalTrust,
event_cache::paginator::PaginatorError,
Expand Down Expand Up @@ -840,6 +840,108 @@ impl Room {

Ok(())
}

/// Subscribes to requests to join this room, using a `listener` to be
/// notified of the changes.
///
/// The current requests to join the room will be emitted immediately
/// when subscribing, along with a [`TaskHandle`] to cancel the
/// subscription.
pub async fn subscribe_to_requests_to_join(
self: Arc<Self>,
listener: Box<dyn RequestsToJoinListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let stream = self.inner.subscribe_to_requests_to_join().await?;

let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
pin_mut!(stream);
while let Some(requests) = stream.next().await {
listener.call(requests.into_iter().map(Into::into).collect());
}
})));

Ok(handle)
}
}

impl From<matrix_sdk::room::request_to_join::RequestToJoinRoom> for RequestToJoin {
fn from(request: matrix_sdk::room::request_to_join::RequestToJoinRoom) -> Self {
Self {
event_id: request.event_id.to_string(),
user_id: request.member_info.user_id.to_string(),
room_id: request.room_id().to_string(),
display_name: request.member_info.display_name.clone(),
avatar_url: request.member_info.avatar_url.as_ref().map(|url| url.to_string()),
reason: request.member_info.reason.clone(),
timestamp: request.timestamp.map(|ts| ts.into()),
is_seen: request.is_seen,
actions: Arc::new(RequestToJoinActions { inner: request }),
}
}
}

/// A listener for receiving new requests to a join a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RequestsToJoinListener: Send + Sync {
fn call(&self, requests_to_join: Vec<RequestToJoin>);
}

/// An FFI representation of a request to join a room.
#[derive(Debug, Clone, uniffi::Record)]
pub struct RequestToJoin {
/// The event id of the event that contains the `knock` membership change.
pub event_id: String,
/// The user id of the user who's requesting to join the room.
pub user_id: String,
/// The room id of the room whose access was requested.
pub room_id: String,
/// The optional display name of the user who's requesting to join the room.
pub display_name: Option<String>,
/// The optional avatar url of the user who's requesting to join the room.
pub avatar_url: Option<String>,
/// An optional reason why the user wants join the room.
pub reason: Option<String>,
/// The timestamp when this request was created.
pub timestamp: Option<u64>,
/// Whether the request to join has been marked as `seen` so it can be
/// filtered by the client.
pub is_seen: bool,
/// A set of actions to perform for this request to join.
pub actions: Arc<RequestToJoinActions>,
}

/// A set of actions to perform for a request to join.
#[derive(Debug, Clone, uniffi::Object)]
pub struct RequestToJoinActions {
inner: matrix_sdk::room::request_to_join::RequestToJoinRoom,
}

#[matrix_sdk_ffi_macros::export]
impl RequestToJoinActions {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing mark_as_seen?

/// Accepts the request to join by inviting the user to the room.
pub async fn accept(&self) -> Result<(), ClientError> {
self.inner.accept().await.map_err(Into::into)
}

/// Declines the request to join by kicking the user from the room with an
/// optional reason.
pub async fn decline(&self, reason: Option<String>) -> Result<(), ClientError> {
self.inner.decline(reason.as_deref()).await.map_err(Into::into)
}

/// Declines the request to join by banning the user from the room with an
/// optional reason.
pub async fn decline_and_ban(&self, reason: Option<String>) -> Result<(), ClientError> {
Copy link
Contributor

@ganfra ganfra Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we want a separate ban action, and let the user chain? in the case the user doesn't have the right for both

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean kick, then ban? Can we ban a user who already left the room?

Also, I'd expect the UI to not display the component that triggers this action rather than controlling trying to workaround the issue here.

Copy link
Contributor

@ganfra ganfra Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I'm stupid. :D

This comment was marked as resolved.

self.inner.decline_and_ban(reason.as_deref()).await.map_err(Into::into)
}

/// Marks the request as 'seen'.
///
/// **IMPORTANT**: this won't update the current reference to this request,
/// a new one with the updated value should be emitted instead.
pub async fn mark_as_seen(&self) -> Result<(), ClientError> {
self.inner.clone().mark_as_seen().await.map_err(Into::into)
}
}

/// Generates a `matrix.to` permalink to the given room alias.
Expand Down
19 changes: 18 additions & 1 deletion crates/matrix-sdk-base/src/deserialized_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use ruma::{
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
},
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
};
use serde::Serialize;
use unicode_normalization::UnicodeNormalization;
Expand Down Expand Up @@ -476,6 +476,23 @@ impl MemberEvent {
.unwrap_or_else(|| self.user_id().localpart()),
)
}

/// The optional reason why the membership changed.
pub fn reason(&self) -> Option<&str> {
match self {
MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(),
MemberEvent::Stripped(e) => e.content.reason.as_deref(),
_ => None,
}
}

/// The optional timestamp for this member event.
pub fn timestamp(&self) -> Option<UInt> {
match self {
MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0),
_ => None,
}
}
}

impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
Expand Down
4 changes: 4 additions & 0 deletions crates/matrix-sdk-base/src/rooms/normal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ pub struct Room {
/// to disk but held in memory.
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
pub latest_encrypted_events: Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>,

/// The event ids for seen request to join room events.
pub seen_requests_to_join_ids: SharedObservable<Option<HashSet<OwnedEventId>>>,
}

/// The room summary containing member counts and members that should be used to
Expand Down Expand Up @@ -255,6 +258,7 @@ impl Room {
Self::MAX_ENCRYPTED_EVENTS,
))),
room_info_notable_update_sender,
seen_requests_to_join_ids: SharedObservable::new(None),
}
}

Expand Down
21 changes: 20 additions & 1 deletion crates/matrix-sdk-base/src/store/memory_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

use std::{
collections::{BTreeMap, BTreeSet, HashMap},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
sync::RwLock as StdRwLock,
};

Expand Down Expand Up @@ -91,6 +91,7 @@ pub struct MemoryStore {
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<QueuedRequest>>>,
dependent_send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<DependentQueuedRequest>>>,
seen_requests_to_join: StdRwLock<BTreeMap<OwnedRoomId, HashSet<OwnedEventId>>>,
}

impl MemoryStore {
Expand Down Expand Up @@ -186,6 +187,13 @@ impl StateStore for MemoryStore {
.get(room_id)
.cloned()
.map(StateStoreDataValue::ComposerDraft),
StateStoreDataKey::SeenRequestsToJoin(room_id) => self
.seen_requests_to_join
.read()
.unwrap()
.get(room_id)
.cloned()
.map(StateStoreDataValue::SeenRequestsToJoin),
})
}

Expand Down Expand Up @@ -239,6 +247,14 @@ impl StateStore for MemoryStore {
.expect("Session data not containing server capabilities"),
);
}
StateStoreDataKey::SeenRequestsToJoin(room_id) => {
self.seen_requests_to_join.write().unwrap().insert(
room_id.to_owned(),
value
.into_seen_join_requests()
.expect("Session data not a set of ignored join requests"),
);
}
}

Ok(())
Expand All @@ -265,6 +281,9 @@ impl StateStore for MemoryStore {
StateStoreDataKey::ComposerDraft(room_id) => {
self.composer_drafts.write().unwrap().remove(room_id);
}
StateStoreDataKey::SeenRequestsToJoin(room_id) => {
self.seen_requests_to_join.write().unwrap().remove(room_id);
}
}
Ok(())
}
Expand Down
17 changes: 16 additions & 1 deletion crates/matrix-sdk-base/src/store/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet, HashMap},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fmt,
sync::Arc,
};
Expand Down Expand Up @@ -1022,6 +1022,9 @@ pub enum StateStoreDataValue {
///
/// [`ComposerDraft`]: Self::ComposerDraft
ComposerDraft(ComposerDraft),

/// A list of requests to join marked as seen in a room.
SeenRequestsToJoin(HashSet<OwnedEventId>),
}

/// Current draft of the composer for the room.
Expand Down Expand Up @@ -1088,6 +1091,11 @@ impl StateStoreDataValue {
pub fn into_server_capabilities(self) -> Option<ServerCapabilities> {
as_variant!(self, Self::ServerCapabilities)
}

/// Get this value if it is the data for the ignored join requests.
pub fn into_seen_join_requests(self) -> Option<HashSet<OwnedEventId>> {
as_variant!(self, Self::SeenRequestsToJoin)
}
}

/// A key for key-value data.
Expand Down Expand Up @@ -1117,6 +1125,9 @@ pub enum StateStoreDataKey<'a> {
///
/// [`ComposerDraft`]: Self::ComposerDraft
ComposerDraft(&'a RoomId),

/// A list of requests to join in a room marked as seen.
SeenRequestsToJoin(&'a RoomId),
}

impl StateStoreDataKey<'_> {
Expand All @@ -1142,6 +1153,10 @@ impl StateStoreDataKey<'_> {
/// Key prefix to use for the [`ComposerDraft`][Self::ComposerDraft]
/// variant.
pub const COMPOSER_DRAFT: &'static str = "composer_draft";

/// Key prefix to use for the
/// [`SeenRequestsToJoin`][Self::SeenRequestsToJoin] variant.
pub const SEEN_REQUESTS_TO_JOIN: &'static str = "seen_requests_to_join";
}

#[cfg(test)]
Expand Down
12 changes: 12 additions & 0 deletions crates/matrix-sdk-indexeddb/src/state_store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,9 @@ impl IndexeddbStateStore {
StateStoreDataKey::ComposerDraft(room_id) => {
self.encode_key(keys::KV, (StateStoreDataKey::COMPOSER_DRAFT, room_id))
}
StateStoreDataKey::SeenRequestsToJoin(room_id) => {
self.encode_key(keys::KV, (StateStoreDataKey::SEEN_REQUESTS_TO_JOIN, room_id))
}
}
}
}
Expand Down Expand Up @@ -537,6 +540,10 @@ impl_state_store!({
.map(|f| self.deserialize_value::<ComposerDraft>(&f))
.transpose()?
.map(StateStoreDataValue::ComposerDraft),
StateStoreDataKey::SeenRequestsToJoin(_) => value
.map(|f| self.deserialize_value::<HashSet<OwnedEventId>>(&f))
.transpose()?
.map(StateStoreDataValue::SeenRequestsToJoin),
};

Ok(value)
Expand Down Expand Up @@ -574,6 +581,11 @@ impl_state_store!({
StateStoreDataKey::ComposerDraft(_) => self.serialize_value(
&value.into_composer_draft().expect("Session data not a composer draft"),
),
StateStoreDataKey::SeenRequestsToJoin(_) => self.serialize_value(
&value
.into_seen_join_requests()
.expect("Session data not a set of ignored requests to join"),
),
};

let tx =
Expand Down
11 changes: 11 additions & 0 deletions crates/matrix-sdk-sqlite/src/state_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ impl SqliteStateStore {
StateStoreDataKey::ComposerDraft(room_id) => {
Cow::Owned(format!("{}:{room_id}", StateStoreDataKey::COMPOSER_DRAFT))
}
StateStoreDataKey::SeenRequestsToJoin(room_id) => {
Cow::Owned(format!("{}:{room_id}", StateStoreDataKey::SEEN_REQUESTS_TO_JOIN))
}
};

self.encode_key(keys::KV_BLOB, &*key_s)
Expand Down Expand Up @@ -995,6 +998,9 @@ impl StateStore for SqliteStateStore {
StateStoreDataKey::ComposerDraft(_) => {
StateStoreDataValue::ComposerDraft(self.deserialize_value(&data)?)
}
StateStoreDataKey::SeenRequestsToJoin(_) => {
StateStoreDataValue::SeenRequestsToJoin(self.deserialize_value(&data)?)
}
})
})
.transpose()
Expand Down Expand Up @@ -1029,6 +1035,11 @@ impl StateStore for SqliteStateStore {
StateStoreDataKey::ComposerDraft(_) => self.serialize_value(
&value.into_composer_draft().expect("Session data not a composer draft"),
)?,
StateStoreDataKey::SeenRequestsToJoin(_) => self.serialize_value(
&value
.into_seen_join_requests()
.expect("Session data not a set of ignored requests to join"),
)?,
};

self.acquire()
Expand Down
Loading
Loading