matrix_sdk_ui/room_list_service/sorters/
recency.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::cmp::Ordering;
16
17use super::{Room, Sorter};
18
19struct RecencyMatcher<F>
20where
21    F: Fn(&Room, &Room) -> (Option<u64>, Option<u64>),
22{
23    recency_stamps: F,
24}
25
26impl<F> RecencyMatcher<F>
27where
28    F: Fn(&Room, &Room) -> (Option<u64>, Option<u64>),
29{
30    fn matches(&self, left: &Room, right: &Room) -> Ordering {
31        if left.room_id() == right.room_id() {
32            // `left` and `right` are the same room. We are comparing the same
33            // `LatestEvent`!
34            //
35            // The way our `Room` types are implemented makes it so they are sharing the
36            // same data, because they are all built from the same store. They can be seen
37            // as shallow clones of each others. In practice it's really great: a `Room` can
38            // never be outdated. However, for the case of sorting rooms, it breaks the
39            // search algorithm. `left` and `right` will have the exact same recency
40            // stamp, so `left` and `right` will always be `Ordering::Equal`. This is
41            // wrong: if `left` is compared with `right` and if they are both the same room,
42            // it means that one of them (either `left`, or `right`, it's not important) has
43            // received an update. The room position is very likely to change. But if they
44            // compare to `Equal`, the position may not change. It actually depends of the
45            // search algorithm used by [`eyeball_im_util::SortBy`].
46            //
47            // Since this room received an update, it is more recent than the previous one
48            // we matched against, so return `Ordering::Greater`.
49            return Ordering::Greater;
50        }
51
52        match (self.recency_stamps)(left, right) {
53            (Some(left_stamp), Some(right_stamp)) => left_stamp.cmp(&right_stamp).reverse(),
54
55            (Some(_), None) => Ordering::Less,
56
57            (None, Some(_)) => Ordering::Greater,
58
59            (None, None) => Ordering::Equal,
60        }
61    }
62}
63
64/// Create a new sorter that will sort two [`Room`] by recency, i.e. by
65/// comparing their [`RoomInfo::recency_stamp`] value. The `Room` with the
66/// newest recency stamp comes first, i.e. newest < oldest.
67///
68/// [`RoomInfo::recency_stamp`]: matrix_sdk_base::RoomInfo::recency_stamp
69pub fn new_sorter() -> impl Sorter {
70    let matcher = RecencyMatcher {
71        recency_stamps: move |left, right| (left.recency_stamp(), right.recency_stamp()),
72    };
73
74    move |left, right| -> Ordering { matcher.matches(left, right) }
75}
76
77#[cfg(test)]
78mod tests {
79    use matrix_sdk::test_utils::logged_in_client_with_server;
80    use matrix_sdk_test::async_test;
81    use ruma::room_id;
82
83    use super::{super::super::filters::new_rooms, *};
84
85    #[async_test]
86    async fn test_with_two_recency_stamps() {
87        let (client, server) = logged_in_client_with_server().await;
88        let [room_a, room_b] =
89            new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server).await;
90
91        // `room_a` has an older recency stamp than `room_b`.
92        {
93            let matcher = RecencyMatcher { recency_stamps: |_left, _right| (Some(1), Some(2)) };
94
95            // `room_a` is greater than `room_b`, i.e. it must come after `room_b`.
96            assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Greater);
97        }
98
99        // `room_b` has an older recency stamp than `room_a`.
100        {
101            let matcher = RecencyMatcher { recency_stamps: |_left, _right| (Some(2), Some(1)) };
102
103            // `room_a` is less than `room_b`, i.e. it must come before `room_b`.
104            assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Less);
105        }
106
107        // `room_a` has an equally old recency stamp than `room_b`.
108        {
109            let matcher = RecencyMatcher { recency_stamps: |_left, _right| (Some(1), Some(1)) };
110
111            assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Equal);
112        }
113    }
114
115    #[async_test]
116    async fn test_with_one_recency_stamp() {
117        let (client, server) = logged_in_client_with_server().await;
118        let [room_a, room_b] =
119            new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server).await;
120
121        // `room_a` has a recency stamp, `room_b` has no recency stamp.
122        {
123            let matcher = RecencyMatcher { recency_stamps: |_left, _right| (Some(1), None) };
124
125            assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Less);
126        }
127
128        // `room_a` has no recency stamp, `room_b` has a recency stamp.
129        {
130            let matcher = RecencyMatcher { recency_stamps: |_left, _right| (None, Some(1)) };
131
132            assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Greater);
133        }
134    }
135
136    #[async_test]
137    async fn test_with_zero_recency_stamp() {
138        let (client, server) = logged_in_client_with_server().await;
139        let [room_a, room_b] =
140            new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server).await;
141
142        // `room_a` and `room_b` has no recency stamp.
143        {
144            let matcher = RecencyMatcher { recency_stamps: |_left, _right| (None, None) };
145
146            assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Equal);
147        }
148    }
149}