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}