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

sdk & ffi: add pin_event and unpin_event fns, as well as power level checks #3750

Merged
merged 5 commits into from
Jul 26, 2024
Merged
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
5 changes: 5 additions & 0 deletions bindings/matrix-sdk-ffi/src/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,11 @@ impl Room {
Ok(self.inner.can_user_send_message(&user_id, message.into()).await?)
}

pub async fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_pin_unpin(&user_id).await?)
}

pub async fn can_user_trigger_room_notification(
&self,
user_id: String,
Expand Down
4 changes: 4 additions & 0 deletions bindings/matrix-sdk-ffi/src/room_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub struct RoomInfo {
/// Events causing mentions/highlights for the user, according to their
/// notification settings.
num_unread_mentions: u64,
/// The currently pinned event ids
pinned_event_ids: Vec<String>,
}

impl RoomInfo {
Expand All @@ -64,6 +66,7 @@ impl RoomInfo {
for (id, level) in power_levels_map.iter() {
user_power_levels.insert(id.to_string(), *level);
}
let pinned_event_ids = room.pinned_events().iter().map(|id| id.to_string()).collect();

Ok(Self {
id: room.room_id().to_string(),
Expand Down Expand Up @@ -109,6 +112,7 @@ impl RoomInfo {
num_unread_messages: room.num_unread_messages(),
num_unread_notifications: room.num_unread_notifications(),
num_unread_mentions: room.num_unread_mentions(),
pinned_event_ids,
})
}
}
20 changes: 20 additions & 0 deletions bindings/matrix-sdk-ffi/src/timeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,26 @@ impl Timeline {
)),
}
}

/// Adds a new pinned event by sending an updated `m.room.pinned_events`
/// event containing the new event id.
///
/// Returns `true` if we sent the request, `false` if the event was already
/// pinned.
async fn pin_event(&self, event_id: String) -> Result<bool, ClientError> {
let event_id = EventId::parse(event_id).map_err(ClientError::from)?;
self.inner.pin_event(&event_id).await.map_err(ClientError::from)
}

/// Adds a new pinned event by sending an updated `m.room.pinned_events`
/// event without the event id we want to remove.
///
/// Returns `true` if we sent the request, `false` if the event wasn't
/// pinned
async fn unpin_event(&self, event_id: String) -> Result<bool, ClientError> {
let event_id = EventId::parse(event_id).map_err(ClientError::from)?;
self.inner.unpin_event(&event_id).await.map_err(ClientError::from)
}
}

#[derive(uniffi::Object)]
Expand Down
5 changes: 5 additions & 0 deletions crates/matrix-sdk-base/src/rooms/members.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ impl RoomMember {
self.can_do_impl(|pls| pls.user_can_send_state(self.user_id(), state_type))
}

/// Whether this user can pin or unpin events based on the power levels.
pub fn can_pin_or_unpin_event(&self) -> bool {
self.can_send_state(StateEventType::RoomPinnedEvents)
}

/// Whether this user can notify everybody in the room by writing `@room` in
/// a message.
///
Expand Down
2 changes: 1 addition & 1 deletion crates/matrix-sdk-ui/src/timeline/event_item/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub(in crate::timeline) struct RemoteEventTimelineItem {
/// Note that currently this ignores threads.
pub read_receipts: IndexMap<OwnedUserId, Receipt>,

/// Whether the event has been sent by the the logged-in user themselves.
/// Whether the event has been sent by the logged-in user themselves.
pub is_own: bool,

/// Whether the item should be highlighted in the timeline.
Expand Down
2 changes: 1 addition & 1 deletion crates/matrix-sdk-ui/src/timeline/inner/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ impl TimelineInnerState {
timestamp: MilliSecondsSinceUnixEpoch::now(),
is_own_event: true,
read_receipts: Default::default(),
// An event sent by ourself is never matched against push rules.
// An event sent by ourselves is never matched against push rules.
is_highlighted: false,
flow: Flow::Local { txn_id, send_handle },
};
Expand Down
37 changes: 37 additions & 0 deletions crates/matrix-sdk-ui/src/timeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ use ruma::{
AddMentions, ForwardThread, OriginalRoomMessageEvent, RoomMessageEventContent,
RoomMessageEventContentWithoutRelation,
},
pinned_events::RoomPinnedEventsEventContent,
redaction::RoomRedactionEventContent,
},
AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
Expand Down Expand Up @@ -857,6 +858,42 @@ impl Timeline {
Ok(false)
}
}

/// Adds a new pinned event by sending an updated `m.room.pinned_events`
/// event containing the new event id.
///
/// Returns `true` if we sent the request, `false` if the event was already
/// pinned.
pub async fn pin_event(&self, event_id: &EventId) -> Result<bool> {
Velin92 marked this conversation as resolved.
Show resolved Hide resolved
let mut pinned_events = self.room().pinned_events();
let event_id = event_id.to_owned();
andybalaam marked this conversation as resolved.
Show resolved Hide resolved
if pinned_events.contains(&event_id) {
Ok(false)
} else {
pinned_events.push(event_id);
let content = RoomPinnedEventsEventContent::new(pinned_events);
self.room().send_state_event(content).await?;
Ok(true)
}
}

/// Adds a new pinned event by sending an updated `m.room.pinned_events`
/// event without the event id we want to remove.
///
/// Returns `true` if we sent the request, `false` if the event wasn't
/// pinned.
pub async fn unpin_event(&self, event_id: &EventId) -> Result<bool> {
Velin92 marked this conversation as resolved.
Show resolved Hide resolved
let mut pinned_events = self.room().pinned_events();
let event_id = event_id.to_owned();
andybalaam marked this conversation as resolved.
Show resolved Hide resolved
if let Some(idx) = pinned_events.iter().position(|e| *e == *event_id) {
pinned_events.remove(idx);
let content = RoomPinnedEventsEventContent::new(pinned_events);
self.room().send_state_event(content).await?;
Ok(true)
} else {
Ok(false)
}
}
}

/// Test helpers, likely not very useful in production.
Expand Down
166 changes: 166 additions & 0 deletions crates/matrix-sdk-ui/tests/integration/timeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,169 @@ async fn test_duplicate_maintains_correct_order() {
let content = items[3].as_event().unwrap().content().as_message().unwrap().body();
assert_eq!(content, "C");
}

#[async_test]
async fn test_pin_event_is_sent_successfully() {
let mut setup = PinningTestSetup::new().await;
let timeline = setup.timeline().await;

setup.mock_sync(false).await;
assert!(!timeline.items().await.is_empty());

// Pinning a remote event succeeds.
setup
.mock_response(ResponseTemplate::new(200).set_body_json(json!({
"event_id": "$42"
})))
.await;

let event_id = setup.event_id();
assert!(timeline.pin_event(event_id).await.unwrap());

setup.reset_server().await;
}

#[async_test]
async fn test_pin_event_is_returning_false_because_is_already_pinned() {
let mut setup = PinningTestSetup::new().await;
let timeline = setup.timeline().await;

setup.mock_sync(true).await;
assert!(!timeline.items().await.is_empty());

let event_id = setup.event_id();
assert!(!timeline.pin_event(event_id).await.unwrap());

setup.reset_server().await;
}

#[async_test]
async fn test_pin_event_is_returning_an_error() {
let mut setup = PinningTestSetup::new().await;
let timeline = setup.timeline().await;

setup.mock_sync(false).await;
assert!(!timeline.items().await.is_empty());

// Pinning a remote event fails.
setup.mock_response(ResponseTemplate::new(400)).await;

let event_id = setup.event_id();
assert!(timeline.pin_event(event_id).await.is_err());

setup.reset_server().await;
}

#[async_test]
async fn test_unpin_event_is_sent_successfully() {
let mut setup = PinningTestSetup::new().await;
let timeline = setup.timeline().await;

setup.mock_sync(true).await;
assert!(!timeline.items().await.is_empty());

// Unpinning a remote event succeeds.
setup
.mock_response(ResponseTemplate::new(200).set_body_json(json!({
"event_id": "$42"
})))
.await;

let event_id = setup.event_id();
assert!(timeline.unpin_event(event_id).await.unwrap());

setup.reset_server().await;
}

#[async_test]
async fn test_unpin_event_is_returning_false_because_is_not_pinned() {
let mut setup = PinningTestSetup::new().await;
let timeline = setup.timeline().await;

setup.mock_sync(false).await;
assert!(!timeline.items().await.is_empty());

let event_id = setup.event_id();
assert!(!timeline.unpin_event(event_id).await.unwrap());

setup.reset_server().await;
}

#[async_test]
async fn test_unpin_event_is_returning_an_error() {
let mut setup = PinningTestSetup::new().await;
let timeline = setup.timeline().await;

setup.mock_sync(true).await;
assert!(!timeline.items().await.is_empty());

// Unpinning a remote event fails.
setup.mock_response(ResponseTemplate::new(400)).await;

let event_id = setup.event_id();
assert!(timeline.unpin_event(event_id).await.is_err());

setup.reset_server().await;
}

struct PinningTestSetup<'a> {
event_id: &'a ruma::EventId,
room_id: &'a ruma::RoomId,
client: matrix_sdk::Client,
server: wiremock::MockServer,
sync_settings: SyncSettings,
sync_builder: SyncResponseBuilder,
}

impl PinningTestSetup<'_> {
async fn new() -> Self {
let room_id = room_id!("!a98sd12bjh:example.org");
let (client, server) = logged_in_client_with_server().await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));

let mut sync_builder = SyncResponseBuilder::new();
let event_id = event_id!("$a");
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));

mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
server.reset().await;

Self { event_id, room_id, client, server, sync_settings, sync_builder }
}

async fn timeline(&self) -> matrix_sdk_ui::Timeline {
let room = self.client.get_room(self.room_id).unwrap();
room.timeline().await.unwrap()
}

async fn reset_server(&self) {
andybalaam marked this conversation as resolved.
Show resolved Hide resolved
self.server.reset().await;
}

async fn mock_response(&self, response: ResponseTemplate) {
Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.pinned_events/.*?"))
.and(header("authorization", "Bearer 1234"))
.respond_with(response)
.mount(&self.server)
.await;
}

async fn mock_sync(&mut self, is_using_pinned_state_event: bool) {
let f = EventFactory::new().sender(user_id!("@a:b.c"));
let mut joined_room_builder = JoinedRoomBuilder::new(self.room_id)
.add_timeline_event(f.text_msg("A").event_id(self.event_id).into_raw_sync());
if is_using_pinned_state_event {
joined_room_builder =
joined_room_builder.add_state_event(StateTestEvent::RoomPinnedEvents);
}
self.sync_builder.add_joined_room(joined_room_builder);
mock_sync(&self.server, self.sync_builder.build_json_sync_response(), None).await;
let _response = self.client.sync_once(self.sync_settings.clone()).await.unwrap();
}

fn event_id(&self) -> &ruma::EventId {
self.event_id
}
}
11 changes: 11 additions & 0 deletions crates/matrix-sdk/src/room/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2332,6 +2332,17 @@ impl Room {
Ok(self.room_power_levels().await?.user_can_send_message(user_id, message))
}

/// Returns true if the user with the given user_id is able to pin or unpin
/// events in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub async fn can_user_pin_unpin(&self, user_id: &UserId) -> Result<bool> {
Ok(self
.room_power_levels()
.await?
.user_can_send_state(user_id, StateEventType::RoomPinnedEvents))
}

/// Returns true if the user with the given user_id is able to trigger a
/// notification in the room.
///
Expand Down
2 changes: 2 additions & 0 deletions testing/matrix-sdk-test/src/sync_builder/test_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub enum StateTestEvent {
RedactedState,
RoomAvatar,
RoomName,
RoomPinnedEvents,
RoomTopic,
Custom(JsonValue),
}
Expand All @@ -53,6 +54,7 @@ impl StateTestEvent {
Self::RedactedState => test_json::sync_events::REDACTED_STATE.to_owned(),
Self::RoomAvatar => test_json::sync_events::ROOM_AVATAR.to_owned(),
Self::RoomName => test_json::sync_events::NAME.to_owned(),
Self::RoomPinnedEvents => test_json::sync_events::PINNED_EVENTS.to_owned(),
Self::RoomTopic => test_json::sync_events::TOPIC.to_owned(),
Self::Custom(json) => json,
}
Expand Down
16 changes: 16 additions & 0 deletions testing/matrix-sdk-test/src/test_json/sync_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,22 @@ pub static NAME_STRIPPED: Lazy<JsonValue> = Lazy::new(|| {
})
});

pub static PINNED_EVENTS: Lazy<JsonValue> = Lazy::new(|| {
json!({
"content": {
"pinned": [ "$a" ]
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.pinned_events",
"unsigned": {
"age": 703422
}
})
});

pub static POWER_LEVELS: Lazy<JsonValue> = Lazy::new(|| {
json!({
"content": {
Expand Down
Loading