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, 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            StateEventContentChange::Redacted(_) => RoomPinnedEventsChange::Changed,
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use assert_matches::assert_matches;
79    use ruma::{
80        events::{
81            StateEventContentChange,
82            room::pinned_events::{
83                RedactedRoomPinnedEventsEventContent, RoomPinnedEventsEventContent,
84            },
85        },
86        owned_event_id,
87    };
88
89    use crate::timeline::event_item::content::pinned_events::RoomPinnedEventsChange;
90
91    #[test]
92    fn redacted_pinned_events_content_has_generic_changes() {
93        let content =
94            StateEventContentChange::Redacted(RedactedRoomPinnedEventsEventContent::new());
95        let ret: RoomPinnedEventsChange = (&content).into();
96        assert_matches!(ret, RoomPinnedEventsChange::Changed);
97    }
98
99    #[test]
100    fn pinned_events_content_with_no_prev_content_returns_added() {
101        let content = StateEventContentChange::Original {
102            content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]),
103            prev_content: None,
104        };
105        let ret: RoomPinnedEventsChange = (&content).into();
106        assert_matches!(ret, RoomPinnedEventsChange::Added);
107    }
108
109    #[test]
110    fn pinned_events_content_with_added_ids_returns_added() {
111        let content = StateEventContentChange::Original {
112            content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]),
113            prev_content: Some(RoomPinnedEventsEventContent::new(Vec::new()).into()),
114        };
115        let ret: RoomPinnedEventsChange = (&content).into();
116        assert_matches!(ret, RoomPinnedEventsChange::Added);
117    }
118
119    #[test]
120    fn pinned_events_content_with_removed_ids_returns_removed() {
121        let content = StateEventContentChange::Original {
122            content: RoomPinnedEventsEventContent::new(Vec::new()),
123            prev_content: Some(
124                RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]).into(),
125            ),
126        };
127        let ret: RoomPinnedEventsChange = (&content).into();
128        assert_matches!(ret, RoomPinnedEventsChange::Removed);
129    }
130
131    #[test]
132    fn pinned_events_content_with_added_and_removed_ids_returns_changed() {
133        let content = StateEventContentChange::Original {
134            content: RoomPinnedEventsEventContent::new(vec![owned_event_id!("$2")]),
135            prev_content: Some(
136                RoomPinnedEventsEventContent::new(vec![owned_event_id!("$1")]).into(),
137            ),
138        };
139        let ret: RoomPinnedEventsChange = (&content).into();
140        assert_matches!(ret, RoomPinnedEventsChange::Changed);
141    }
142
143    #[test]
144    fn pinned_events_content_with_changed_order_returns_changed() {
145        let content = StateEventContentChange::Original {
146            content: RoomPinnedEventsEventContent::new(vec![
147                owned_event_id!("$2"),
148                owned_event_id!("$1"),
149            ]),
150            prev_content: Some(
151                RoomPinnedEventsEventContent::new(vec![
152                    owned_event_id!("$1"),
153                    owned_event_id!("$2"),
154                ])
155                .into(),
156            ),
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        let content = StateEventContentChange::Original {
168            content: RoomPinnedEventsEventContent::new(vec![
169                owned_event_id!("$1"),
170                owned_event_id!("$2"),
171            ]),
172            prev_content: Some(
173                RoomPinnedEventsEventContent::new(vec![
174                    owned_event_id!("$1"),
175                    owned_event_id!("$2"),
176                ])
177                .into(),
178            ),
179        };
180        let ret: RoomPinnedEventsChange = (&content).into();
181        assert_matches!(ret, RoomPinnedEventsChange::Changed);
182    }
183}