diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/remote.rs b/crates/matrix-sdk-ui/src/timeline/event_item/remote.rs index a17979b4a64..f25eb010652 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/remote.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/remote.rs @@ -44,7 +44,7 @@ pub(in crate::timeline) struct RemoteEventTimelineItem { /// Note that currently this ignores threads. pub read_receipts: IndexMap, - /// 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. diff --git a/crates/matrix-sdk-ui/src/timeline/inner/state.rs b/crates/matrix-sdk-ui/src/timeline/inner/state.rs index c273dcd90c0..8a89d18fa63 100644 --- a/crates/matrix-sdk-ui/src/timeline/inner/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/inner/state.rs @@ -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 }, }; diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 572534e5fed..7af5aa15bf6 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -50,6 +50,7 @@ use ruma::{ AddMentions, ForwardThread, OriginalRoomMessageEvent, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, }, + pinned_events::RoomPinnedEventsEventContent, redaction::RoomRedactionEventContent, }, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, @@ -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 { + let mut pinned_events = self.room().pinned_events(); + let event_id = event_id.to_owned(); + 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 { + let mut pinned_events = self.room().pinned_events(); + let event_id = event_id.to_owned(); + 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. diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index e419f7dd1ba..e117a2eb040 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -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) { + 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 + } +} diff --git a/testing/matrix-sdk-test/src/sync_builder/test_event.rs b/testing/matrix-sdk-test/src/sync_builder/test_event.rs index 78f32a084ea..d38e4ed8229 100644 --- a/testing/matrix-sdk-test/src/sync_builder/test_event.rs +++ b/testing/matrix-sdk-test/src/sync_builder/test_event.rs @@ -28,6 +28,7 @@ pub enum StateTestEvent { RedactedState, RoomAvatar, RoomName, + RoomPinnedEvents, RoomTopic, Custom(JsonValue), } @@ -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, } diff --git a/testing/matrix-sdk-test/src/test_json/sync_events.rs b/testing/matrix-sdk-test/src/test_json/sync_events.rs index 3b1abe799bb..1822d576302 100644 --- a/testing/matrix-sdk-test/src/test_json/sync_events.rs +++ b/testing/matrix-sdk-test/src/test_json/sync_events.rs @@ -317,6 +317,22 @@ pub static NAME_STRIPPED: Lazy = Lazy::new(|| { }) }); +pub static PINNED_EVENTS: Lazy = 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 = Lazy::new(|| { json!({ "content": {