From c83fa3a532c950132d2d23698fd8a687640b0905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 6 Aug 2024 09:46:07 +0200 Subject: [PATCH] sdk-ui: move conversion between `Content::RoomPinnedEvents` from `ffi` to `sdk-ui` --- .../matrix-sdk-ffi/src/timeline/content.rs | 63 +----- .../src/timeline/event_item/content/mod.rs | 3 + .../event_item/content/pinned_events.rs | 192 ++++++++++++++++++ .../src/timeline/event_item/mod.rs | 4 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 3 +- 5 files changed, 203 insertions(+), 62 deletions(-) create mode 100644 crates/matrix-sdk-ui/src/timeline/event_item/content/pinned_events.rs diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index 25eacb2d9..ffe2cecb5 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -12,17 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, -}; +use std::{collections::HashMap, sync::Arc}; use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes}; -use matrix_sdk_ui::timeline::{PollResult, TimelineDetails}; -use ruma::{ - events::room::{message::RoomMessageEventContentWithoutRelation, MediaSource}, - OwnedEventId, -}; +use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails}; +use ruma::events::room::{message::RoomMessageEventContentWithoutRelation, MediaSource}; use tracing::warn; use super::ProfileDetails; @@ -355,13 +349,6 @@ pub enum OtherState { Custom { event_type: String }, } -#[derive(Clone, uniffi::Enum)] -pub enum RoomPinnedEventsChange { - Added, - Removed, - Changed, -} - impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherState { fn from(content: &matrix_sdk_ui::timeline::AnyOtherFullStateEventContent) -> Self { use matrix_sdk::ruma::events::FullStateEventContent as FullContent; @@ -394,49 +381,7 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat }; Self::RoomName { name } } - Content::RoomPinnedEvents(c) => { - match c { - FullContent::Original { content, prev_content } => { - let change = if let Some(prev_content) = prev_content { - let mut new_pinned: HashSet<&OwnedEventId> = - HashSet::from_iter(&content.pinned); - if let Some(old_pinned) = &prev_content.pinned { - let mut still_pinned: HashSet<&OwnedEventId> = - HashSet::from_iter(old_pinned); - - // Newly added elements will be kept in new_pinned, previous ones in - // still_pinned instead - still_pinned.retain(|item| new_pinned.remove(item)); - - let added = !new_pinned.is_empty(); - let removed = still_pinned.len() < old_pinned.len(); - if added && removed { - RoomPinnedEventsChange::Changed - } else if added { - RoomPinnedEventsChange::Added - } else if removed { - RoomPinnedEventsChange::Removed - } else { - // Any other case - RoomPinnedEventsChange::Changed - } - } else { - // We don't know the previous state, so let's assume a generic - // change - RoomPinnedEventsChange::Changed - } - } else { - // If there is no previous content we can assume the first pinned event - // id was just added - RoomPinnedEventsChange::Added - }; - Self::RoomPinnedEvents { change } - } - FullContent::Redacted(_) => { - Self::RoomPinnedEvents { change: RoomPinnedEventsChange::Changed } - } - } - } + Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() }, Content::RoomPowerLevels(c) => match c { FullContent::Original { content, prev_content } => Self::RoomPowerLevels { users: power_level_user_changes(content, prev_content) diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index d36c33324..e533fc61d 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -58,6 +58,9 @@ use tracing::warn; use crate::timeline::{polls::PollState, TimelineItem}; mod message; +pub(crate) mod pinned_events; + +pub use pinned_events::RoomPinnedEventsChange; pub use self::message::{InReplyToDetails, Message, RepliedToEvent}; diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/pinned_events.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/pinned_events.rs new file mode 100644 index 000000000..31e1c7c24 --- /dev/null +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/pinned_events.rs @@ -0,0 +1,192 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; + +use ruma::{ + events::{room::pinned_events::RoomPinnedEventsEventContent, FullStateEventContent}, + OwnedEventId, +}; + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +/// The type of change between the previous and current pinned events. +pub enum RoomPinnedEventsChange { + /// Only new event ids were added. + Added, + /// Only event ids were removed. + Removed, + /// Some change other than only adding or only removing ids happened. + Changed, +} + +impl From<&FullStateEventContent> for RoomPinnedEventsChange { + fn from(value: &FullStateEventContent) -> Self { + match value { + FullStateEventContent::Original { content, prev_content } => { + if let Some(prev_content) = prev_content { + let mut new_pinned: HashSet<&OwnedEventId> = + HashSet::from_iter(&content.pinned); + if let Some(old_pinned) = &prev_content.pinned { + let mut still_pinned: HashSet<&OwnedEventId> = + HashSet::from_iter(old_pinned); + + // Newly added elements will be kept in new_pinned, previous ones in + // still_pinned instead + still_pinned.retain(|item| new_pinned.remove(item)); + + let added = !new_pinned.is_empty(); + let removed = still_pinned.len() < old_pinned.len(); + if added && removed { + RoomPinnedEventsChange::Changed + } else if added { + RoomPinnedEventsChange::Added + } else if removed { + RoomPinnedEventsChange::Removed + } else { + // Any other case + RoomPinnedEventsChange::Changed + } + } else { + // We don't know the previous state, so let's assume a generic change + RoomPinnedEventsChange::Changed + } + } else { + // If there is no previous content we can assume the first pinned event id was + // just added + RoomPinnedEventsChange::Added + } + } + FullStateEventContent::Redacted(_) => RoomPinnedEventsChange::Changed, + } + } +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use ruma::{ + events::{ + room::pinned_events::{ + PossiblyRedactedRoomPinnedEventsEventContent, RedactedRoomPinnedEventsEventContent, + RoomPinnedEventsEventContent, + }, + FullStateEventContent, + }, + owned_event_id, + serde::Raw, + }; + use serde_json::json; + + use crate::timeline::event_item::content::pinned_events::RoomPinnedEventsChange; + + #[test] + fn redacted_pinned_events_content_has_generic_changes() { + let content = FullStateEventContent::Redacted(RedactedRoomPinnedEventsEventContent::new()); + let ret: RoomPinnedEventsChange = (&content).into(); + assert_matches!(ret, RoomPinnedEventsChange::Changed); + } + + #[test] + fn pinned_events_content_with_no_prev_content_returns_added() { + let content = FullStateEventContent::Original { + content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]), + prev_content: None, + }; + let ret: RoomPinnedEventsChange = (&content).into(); + assert_matches!(ret, RoomPinnedEventsChange::Added); + } + + #[test] + fn pinned_events_content_with_added_ids_returns_added() { + // This is the only way I found to create the PossiblyRedacted content + let prev_content = possibly_redacted_content(Vec::new()); + let content = FullStateEventContent::Original { + content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]), + prev_content, + }; + let ret: RoomPinnedEventsChange = (&content).into(); + assert_matches!(ret, RoomPinnedEventsChange::Added); + } + + #[test] + fn pinned_events_content_with_removed_ids_returns_removed() { + // This is the only way I found to create the PossiblyRedacted content + let prev_content = possibly_redacted_content(vec!["$1"]); + let content = FullStateEventContent::Original { + content: RoomPinnedEventsEventContent::new(Vec::new()), + prev_content, + }; + let ret: RoomPinnedEventsChange = (&content).into(); + assert_matches!(ret, RoomPinnedEventsChange::Removed); + } + + #[test] + fn pinned_events_content_with_added_and_removed_ids_returns_changed() { + // This is the only way I found to create the PossiblyRedacted content + let prev_content = possibly_redacted_content(vec!["$1"]); + let content = FullStateEventContent::Original { + content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$2")]), + prev_content, + }; + let ret: RoomPinnedEventsChange = (&content).into(); + assert_matches!(ret, RoomPinnedEventsChange::Changed); + } + + #[test] + fn pinned_events_content_with_changed_order_returns_changed() { + // This is the only way I found to create the PossiblyRedacted content + let prev_content = possibly_redacted_content(vec!["$1", "$2"]); + let content = FullStateEventContent::Original { + content: RoomPinnedEventsEventContent::new(vec![ + owned_event_id!("$2"), + owned_event_id!("$1"), + ]), + prev_content, + }; + let ret: RoomPinnedEventsChange = (&content).into(); + assert_matches!(ret, RoomPinnedEventsChange::Changed); + } + + #[test] + fn pinned_events_content_with_no_changes_returns_changed() { + // Returning Changed is counter-intuitive, but it makes no sense to display in + // the timeline 'UserFoo didn't change anything in the pinned events' + + // This is the only way I found to create the PossiblyRedacted content + let prev_content = possibly_redacted_content(vec!["$1", "$2"]); + let content = FullStateEventContent::Original { + content: RoomPinnedEventsEventContent::new(vec![ + owned_event_id!("$1"), + owned_event_id!("$2"), + ]), + prev_content, + }; + let ret: RoomPinnedEventsChange = (&content).into(); + assert_matches!(ret, RoomPinnedEventsChange::Changed); + } + + fn possibly_redacted_content( + ids: Vec<&str>, + ) -> Option { + // This is the only way I found to create the PossiblyRedacted content + Raw::new(&json!({ + "pinned": ids, + })) + .unwrap() + .cast::() + .deserialize() + .ok() + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 7a024398b..acd1f8ba5 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -41,8 +41,8 @@ mod remote; pub use self::{ content::{ AnyOtherFullStateEventContent, EncryptedMessage, InReplyToDetails, MemberProfileChange, - MembershipChange, Message, OtherState, RepliedToEvent, RoomMembershipChange, Sticker, - TimelineItemContent, + MembershipChange, Message, OtherState, RepliedToEvent, RoomMembershipChange, + RoomPinnedEventsChange, Sticker, TimelineItemContent, }, local::EventSendState, }; diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index e26f83082..568df1857 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -94,7 +94,8 @@ pub use self::{ AnyOtherFullStateEventContent, EncryptedMessage, EventItemOrigin, EventSendState, EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, Message, OtherState, Profile, ReactionInfo, ReactionsByKeyBySender, RepliedToEvent, - RoomMembershipChange, Sticker, TimelineDetails, TimelineEventItemId, TimelineItemContent, + RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineDetails, + TimelineEventItemId, TimelineItemContent, }, event_type_filter::TimelineEventTypeFilter, inner::default_event_filter,