Skip to main content

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    OwnedEventId,
19    events::{StateEventContentChange, room::pinned_events::RoomPinnedEventsEventContent},
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<&StateEventContentChange<RoomPinnedEventsEventContent>> for RoomPinnedEventsChange {
35    fn from(value: &StateEventContentChange<RoomPinnedEventsEventContent>) -> Self {
36        match value {
37            StateEventContentChange::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,
46                        // previous ones in 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
63                        // generic change
64                        RoomPinnedEventsChange::Changed
65                    }
66                } else {
67                    // If there is no previous content we can assume the first
68                    // pinned event id was just added
69                    RoomPinnedEventsChange::Added
70                }
71            }
72            StateEventContentChange::Redacted(_) => RoomPinnedEventsChange::Changed,
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use assert_matches::assert_matches;
80    use ruma::{
81        events::{
82            StateEventContentChange,
83            room::pinned_events::{
84                RedactedRoomPinnedEventsEventContent, RoomPinnedEventsEventContent,
85            },
86        },
87        owned_event_id,
88    };
89
90    use crate::timeline::event_item::content::pinned_events::RoomPinnedEventsChange;
91
92    #[test]
93    fn redacted_pinned_events_content_has_generic_changes() {
94        let content =
95            StateEventContentChange::Redacted(RedactedRoomPinnedEventsEventContent::new());
96        let ret: RoomPinnedEventsChange = (&content).into();
97        assert_matches!(ret, RoomPinnedEventsChange::Changed);
98    }
99
100    #[test]
101    fn pinned_events_content_with_no_prev_content_returns_added() {
102        let content = StateEventContentChange::Original {
103            content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]),
104            prev_content: None,
105        };
106        let ret: RoomPinnedEventsChange = (&content).into();
107        assert_matches!(ret, RoomPinnedEventsChange::Added);
108    }
109
110    #[test]
111    fn pinned_events_content_with_added_ids_returns_added() {
112        let content = StateEventContentChange::Original {
113            content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]),
114            prev_content: Some(RoomPinnedEventsEventContent::new(Vec::new()).into()),
115        };
116        let ret: RoomPinnedEventsChange = (&content).into();
117        assert_matches!(ret, RoomPinnedEventsChange::Added);
118    }
119
120    #[test]
121    fn pinned_events_content_with_removed_ids_returns_removed() {
122        let content = StateEventContentChange::Original {
123            content: RoomPinnedEventsEventContent::new(Vec::new()),
124            prev_content: Some(
125                RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]).into(),
126            ),
127        };
128        let ret: RoomPinnedEventsChange = (&content).into();
129        assert_matches!(ret, RoomPinnedEventsChange::Removed);
130    }
131
132    #[test]
133    fn pinned_events_content_with_added_and_removed_ids_returns_changed() {
134        let content = StateEventContentChange::Original {
135            content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$2")]),
136            prev_content: Some(
137                RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]).into(),
138            ),
139        };
140        let ret: RoomPinnedEventsChange = (&content).into();
141        assert_matches!(ret, RoomPinnedEventsChange::Changed);
142    }
143
144    #[test]
145    fn pinned_events_content_with_changed_order_returns_changed() {
146        let content = StateEventContentChange::Original {
147            content: RoomPinnedEventsEventContent::new(vec![
148                owned_event_id!("$2"),
149                owned_event_id!("$1"),
150            ]),
151            prev_content: Some(
152                RoomPinnedEventsEventContent::new(vec![
153                    owned_event_id!("$1"),
154                    owned_event_id!("$2"),
155                ])
156                .into(),
157            ),
158        };
159        let ret: RoomPinnedEventsChange = (&content).into();
160        assert_matches!(ret, RoomPinnedEventsChange::Changed);
161    }
162
163    #[test]
164    fn pinned_events_content_with_no_changes_returns_changed() {
165        // Returning Changed is counter-intuitive, but it makes no sense to
166        // display in the timeline 'UserFoo didn't change anything in the pinned
167        // events'
168
169        let content = StateEventContentChange::Original {
170            content: RoomPinnedEventsEventContent::new(vec![
171                owned_event_id!("$1"),
172                owned_event_id!("$2"),
173            ]),
174            prev_content: Some(
175                RoomPinnedEventsEventContent::new(vec![
176                    owned_event_id!("$1"),
177                    owned_event_id!("$2"),
178                ])
179                .into(),
180            ),
181        };
182        let ret: RoomPinnedEventsChange = (&content).into();
183        assert_matches!(ret, RoomPinnedEventsChange::Changed);
184    }
185}