1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
1415use std::collections::HashSet;
1617use ruma::{
18 events::{room::pinned_events::RoomPinnedEventsEventContent, FullStateEventContent},
19 OwnedEventId,
20};
2122#[derive(Clone, Debug)]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
24/// The type of change between the previous and current pinned events.
25pub enum RoomPinnedEventsChange {
26/// Only new event ids were added.
27Added,
28/// Only event ids were removed.
29Removed,
30/// Some change other than only adding or only removing ids happened.
31Changed,
32}
3334impl From<&FullStateEventContent<RoomPinnedEventsEventContent>> for RoomPinnedEventsChange {
35fn from(value: &FullStateEventContent<RoomPinnedEventsEventContent>) -> Self {
36match value {
37 FullStateEventContent::Original { content, prev_content } => {
38if let Some(prev_content) = prev_content {
39let mut new_pinned: HashSet<&OwnedEventId> =
40 HashSet::from_iter(&content.pinned);
41if let Some(old_pinned) = &prev_content.pinned {
42let mut still_pinned: HashSet<&OwnedEventId> =
43 HashSet::from_iter(old_pinned);
4445// Newly added elements will be kept in new_pinned, previous ones in
46 // still_pinned instead
47still_pinned.retain(|item| new_pinned.remove(item));
4849let added = !new_pinned.is_empty();
50let removed = still_pinned.len() < old_pinned.len();
51if added && removed {
52 RoomPinnedEventsChange::Changed
53 } else if added {
54 RoomPinnedEventsChange::Added
55 } else if removed {
56 RoomPinnedEventsChange::Removed
57 } else {
58// Any other case
59RoomPinnedEventsChange::Changed
60 }
61 } else {
62// We don't know the previous state, so let's assume a generic change
63RoomPinnedEventsChange::Changed
64 }
65 } else {
66// If there is no previous content we can assume the first pinned event id was
67 // just added
68RoomPinnedEventsChange::Added
69 }
70 }
71 FullStateEventContent::Redacted(_) => RoomPinnedEventsChange::Changed,
72 }
73 }
74}
7576#[cfg(test)]
77mod tests {
78use assert_matches::assert_matches;
79use ruma::{
80 events::{
81 room::pinned_events::{
82 PossiblyRedactedRoomPinnedEventsEventContent, RedactedRoomPinnedEventsEventContent,
83 RoomPinnedEventsEventContent,
84 },
85 FullStateEventContent,
86 },
87 owned_event_id,
88 serde::Raw,
89 };
90use serde_json::json;
9192use crate::timeline::event_item::content::pinned_events::RoomPinnedEventsChange;
9394#[test]
95fn redacted_pinned_events_content_has_generic_changes() {
96let content = FullStateEventContent::Redacted(RedactedRoomPinnedEventsEventContent::new());
97let ret: RoomPinnedEventsChange = (&content).into();
98assert_matches!(ret, RoomPinnedEventsChange::Changed);
99 }
100101#[test]
102fn pinned_events_content_with_no_prev_content_returns_added() {
103let content = FullStateEventContent::Original {
104 content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]),
105 prev_content: None,
106 };
107let ret: RoomPinnedEventsChange = (&content).into();
108assert_matches!(ret, RoomPinnedEventsChange::Added);
109 }
110111#[test]
112fn pinned_events_content_with_added_ids_returns_added() {
113// This is the only way I found to create the PossiblyRedacted content
114let prev_content = possibly_redacted_content(Vec::new());
115let content = FullStateEventContent::Original {
116 content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]),
117 prev_content,
118 };
119let ret: RoomPinnedEventsChange = (&content).into();
120assert_matches!(ret, RoomPinnedEventsChange::Added);
121 }
122123#[test]
124fn pinned_events_content_with_removed_ids_returns_removed() {
125// This is the only way I found to create the PossiblyRedacted content
126let prev_content = possibly_redacted_content(vec!["$1"]);
127let content = FullStateEventContent::Original {
128 content: RoomPinnedEventsEventContent::new(Vec::new()),
129 prev_content,
130 };
131let ret: RoomPinnedEventsChange = (&content).into();
132assert_matches!(ret, RoomPinnedEventsChange::Removed);
133 }
134135#[test]
136fn pinned_events_content_with_added_and_removed_ids_returns_changed() {
137// This is the only way I found to create the PossiblyRedacted content
138let prev_content = possibly_redacted_content(vec!["$1"]);
139let content = FullStateEventContent::Original {
140 content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$2")]),
141 prev_content,
142 };
143let ret: RoomPinnedEventsChange = (&content).into();
144assert_matches!(ret, RoomPinnedEventsChange::Changed);
145 }
146147#[test]
148fn pinned_events_content_with_changed_order_returns_changed() {
149// This is the only way I found to create the PossiblyRedacted content
150let prev_content = possibly_redacted_content(vec!["$1", "$2"]);
151let content = FullStateEventContent::Original {
152 content: RoomPinnedEventsEventContent::new(vec![
153owned_event_id!("$2"),
154owned_event_id!("$1"),
155 ]),
156 prev_content,
157 };
158let ret: RoomPinnedEventsChange = (&content).into();
159assert_matches!(ret, RoomPinnedEventsChange::Changed);
160 }
161162#[test]
163fn pinned_events_content_with_no_changes_returns_changed() {
164// Returning Changed is counter-intuitive, but it makes no sense to display in
165 // the timeline 'UserFoo didn't change anything in the pinned events'
166167 // This is the only way I found to create the PossiblyRedacted content
168let prev_content = possibly_redacted_content(vec!["$1", "$2"]);
169let content = FullStateEventContent::Original {
170 content: RoomPinnedEventsEventContent::new(vec![
171owned_event_id!("$1"),
172owned_event_id!("$2"),
173 ]),
174 prev_content,
175 };
176let ret: RoomPinnedEventsChange = (&content).into();
177assert_matches!(ret, RoomPinnedEventsChange::Changed);
178 }
179180fn possibly_redacted_content(
181 ids: Vec<&str>,
182 ) -> Option<PossiblyRedactedRoomPinnedEventsEventContent> {
183// This is the only way I found to create the PossiblyRedacted content
184Raw::new(&json!({
185"pinned": ids,
186 }))
187 .unwrap()
188 .cast::<PossiblyRedactedRoomPinnedEventsEventContent>()
189 .deserialize()
190 .ok()
191 }
192}