matrix_sdk_ui/timeline/event_item/content/
pinned_events.rs

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.
14
15use std::collections::HashSet;
16
17use ruma::{
18    events::{room::pinned_events::RoomPinnedEventsEventContent, FullStateEventContent},
19    OwnedEventId,
20};
21
22#[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.
27    Added,
28    /// Only event ids were removed.
29    Removed,
30    /// Some change other than only adding or only removing ids happened.
31    Changed,
32}
33
34impl From<&FullStateEventContent<RoomPinnedEventsEventContent>> for RoomPinnedEventsChange {
35    fn from(value: &FullStateEventContent<RoomPinnedEventsEventContent>) -> Self {
36        match value {
37            FullStateEventContent::Original { content, prev_content } => {
38                if let Some(prev_content) = prev_content {
39                    let mut new_pinned: HashSet<&OwnedEventId> =
40                        HashSet::from_iter(&content.pinned);
41                    if let Some(old_pinned) = &prev_content.pinned {
42                        let mut still_pinned: HashSet<&OwnedEventId> =
43                            HashSet::from_iter(old_pinned);
44
45                        // Newly added elements will be kept in new_pinned, previous ones in
46                        // still_pinned instead
47                        still_pinned.retain(|item| new_pinned.remove(item));
48
49                        let added = !new_pinned.is_empty();
50                        let removed = still_pinned.len() < old_pinned.len();
51                        if 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
59                            RoomPinnedEventsChange::Changed
60                        }
61                    } else {
62                        // We don't know the previous state, so let's assume a generic change
63                        RoomPinnedEventsChange::Changed
64                    }
65                } else {
66                    // If there is no previous content we can assume the first pinned event id was
67                    // just added
68                    RoomPinnedEventsChange::Added
69                }
70            }
71            FullStateEventContent::Redacted(_) => RoomPinnedEventsChange::Changed,
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use assert_matches::assert_matches;
79    use ruma::{
80        events::{
81            room::pinned_events::{
82                PossiblyRedactedRoomPinnedEventsEventContent, RedactedRoomPinnedEventsEventContent,
83                RoomPinnedEventsEventContent,
84            },
85            FullStateEventContent,
86        },
87        owned_event_id,
88        serde::Raw,
89    };
90    use serde_json::json;
91
92    use crate::timeline::event_item::content::pinned_events::RoomPinnedEventsChange;
93
94    #[test]
95    fn redacted_pinned_events_content_has_generic_changes() {
96        let content = FullStateEventContent::Redacted(RedactedRoomPinnedEventsEventContent::new());
97        let ret: RoomPinnedEventsChange = (&content).into();
98        assert_matches!(ret, RoomPinnedEventsChange::Changed);
99    }
100
101    #[test]
102    fn pinned_events_content_with_no_prev_content_returns_added() {
103        let content = FullStateEventContent::Original {
104            content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]),
105            prev_content: None,
106        };
107        let ret: RoomPinnedEventsChange = (&content).into();
108        assert_matches!(ret, RoomPinnedEventsChange::Added);
109    }
110
111    #[test]
112    fn pinned_events_content_with_added_ids_returns_added() {
113        // This is the only way I found to create the PossiblyRedacted content
114        let prev_content = possibly_redacted_content(Vec::new());
115        let content = FullStateEventContent::Original {
116            content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]),
117            prev_content,
118        };
119        let ret: RoomPinnedEventsChange = (&content).into();
120        assert_matches!(ret, RoomPinnedEventsChange::Added);
121    }
122
123    #[test]
124    fn pinned_events_content_with_removed_ids_returns_removed() {
125        // This is the only way I found to create the PossiblyRedacted content
126        let prev_content = possibly_redacted_content(vec!["$1"]);
127        let content = FullStateEventContent::Original {
128            content: RoomPinnedEventsEventContent::new(Vec::new()),
129            prev_content,
130        };
131        let ret: RoomPinnedEventsChange = (&content).into();
132        assert_matches!(ret, RoomPinnedEventsChange::Removed);
133    }
134
135    #[test]
136    fn pinned_events_content_with_added_and_removed_ids_returns_changed() {
137        // This is the only way I found to create the PossiblyRedacted content
138        let prev_content = possibly_redacted_content(vec!["$1"]);
139        let content = FullStateEventContent::Original {
140            content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$2")]),
141            prev_content,
142        };
143        let ret: RoomPinnedEventsChange = (&content).into();
144        assert_matches!(ret, RoomPinnedEventsChange::Changed);
145    }
146
147    #[test]
148    fn pinned_events_content_with_changed_order_returns_changed() {
149        // This is the only way I found to create the PossiblyRedacted content
150        let prev_content = possibly_redacted_content(vec!["$1", "$2"]);
151        let content = FullStateEventContent::Original {
152            content: RoomPinnedEventsEventContent::new(vec![
153                owned_event_id!("$2"),
154                owned_event_id!("$1"),
155            ]),
156            prev_content,
157        };
158        let ret: RoomPinnedEventsChange = (&content).into();
159        assert_matches!(ret, RoomPinnedEventsChange::Changed);
160    }
161
162    #[test]
163    fn 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'
166
167        // This is the only way I found to create the PossiblyRedacted content
168        let prev_content = possibly_redacted_content(vec!["$1", "$2"]);
169        let content = FullStateEventContent::Original {
170            content: RoomPinnedEventsEventContent::new(vec![
171                owned_event_id!("$1"),
172                owned_event_id!("$2"),
173            ]),
174            prev_content,
175        };
176        let ret: RoomPinnedEventsChange = (&content).into();
177        assert_matches!(ret, RoomPinnedEventsChange::Changed);
178    }
179
180    fn possibly_redacted_content(
181        ids: Vec<&str>,
182    ) -> Option<PossiblyRedactedRoomPinnedEventsEventContent> {
183        // This is the only way I found to create the PossiblyRedacted content
184        Raw::new(&json!({
185            "pinned": ids,
186        }))
187        .unwrap()
188        .cast::<PossiblyRedactedRoomPinnedEventsEventContent>()
189        .deserialize()
190        .ok()
191    }
192}