1use std::{collections::BTreeMap, fmt::Debug};
2
3use futures::{StreamExt, future::join3, stream::select};
4use ruma::{EventId, OwnedEventId, RoomId, UserId, events::receipt::ReceiptThread};
5use serde::Serialize;
6use tuwunel_core::{
7 Result, implement, trace,
8 utils::{
9 stream::{BroadbandExt, ReadyExt, TryIgnore},
10 u64_from_u8,
11 },
12};
13use tuwunel_database::{
14 Deserialized, Ignore, IgnoreAll, Interfix, KeyBuf, deserialize_from_slice as deserialize_key,
15};
16
17type ThreadCounts = BTreeMap<OwnedEventId, (u64, u64)>;
19
20type ThreadLastReads = BTreeMap<OwnedEventId, u64>;
24
25#[implement(super::Service)]
30#[tracing::instrument(level = "debug", skip(self))]
31pub async fn reset_notification_counts(&self, user_id: &UserId, room_id: &RoomId) {
32 let count = self.services.globals.next_count();
33
34 let userroom_id = (user_id, room_id);
35
36 self.reset_notification_count(room_id, user_id, userroom_id)
37 .await;
38
39 self.db
40 .userroomid_highlightcount
41 .put(userroom_id, 0_u64);
42
43 let roomuser_id = (room_id, user_id);
44 self.db
45 .roomuserid_lastnotificationread
46 .put(roomuser_id, *count);
47
48 let removed = self.clear_suppressed_room(user_id, room_id);
49 if removed > 0 {
50 trace!(?user_id, ?room_id, removed, "Cleared suppressed push events after read");
51 }
52}
53
54#[implement(super::Service)]
55async fn reset_notification_count<K>(&self, room_id: &RoomId, user_id: &UserId, key: K)
56where
57 K: Serialize + Debug + Send + Sync,
58{
59 let _lock = self
62 .notification_increment_mutex
63 .lock(&(room_id.to_owned(), user_id.to_owned()))
64 .await;
65
66 self.db
67 .userroomid_notificationcount
68 .put(key, 0_u64);
69}
70
71#[implement(super::Service)]
75#[tracing::instrument(level = "debug", skip(self))]
76pub async fn reset_thread_notification_counts(
77 &self,
78 user_id: &UserId,
79 room_id: &RoomId,
80 thread_root: &EventId,
81) {
82 let count = self.services.globals.next_count();
83
84 let userroom_thread = (user_id, room_id, thread_root);
85
86 self.reset_notification_count(room_id, user_id, userroom_thread)
87 .await;
88
89 self.db
90 .userroomid_highlightcount
91 .put(userroom_thread, 0_u64);
92
93 let roomuser_thread = (room_id, user_id, thread_root);
94 self.db
95 .roomuserid_lastnotificationread
96 .put(roomuser_thread, *count);
97}
98
99#[implement(super::Service)]
105#[tracing::instrument(level = "debug", skip(self))]
106pub async fn clear_all_thread_notification_counts(&self, user_id: &UserId, room_id: &RoomId) {
107 let userroom_prefix = (user_id, room_id, Interfix);
108 let roomuser_prefix = (room_id, user_id, Interfix);
109
110 let highlights = self
111 .db
112 .userroomid_highlightcount
113 .del_prefix(&userroom_prefix);
114
115 let last_reads = self
116 .db
117 .roomuserid_lastnotificationread
118 .del_prefix(&roomuser_prefix);
119
120 let notifications = async {
121 let _lock = self
122 .notification_increment_mutex
123 .lock(&(room_id.to_owned(), user_id.to_owned()))
124 .await;
125
126 self.db
127 .userroomid_notificationcount
128 .del_prefix(&userroom_prefix)
129 .await;
130 };
131
132 join3(notifications, highlights, last_reads).await;
133}
134
135#[implement(super::Service)]
140pub async fn reset_notification_counts_for_thread(
141 &self,
142 user_id: &UserId,
143 room_id: &RoomId,
144 thread: &ReceiptThread,
145) {
146 match thread {
147 | ReceiptThread::Main =>
148 self.reset_notification_counts(user_id, room_id)
149 .await,
150 | ReceiptThread::Thread(root) =>
151 self.reset_thread_notification_counts(user_id, room_id, root)
152 .await,
153 | _ => {
154 self.reset_notification_counts(user_id, room_id)
155 .await;
156
157 self.clear_all_thread_notification_counts(user_id, room_id)
158 .await;
159 },
160 }
161}
162
163#[implement(super::Service)]
164#[tracing::instrument(level = "debug", skip(self), ret(level = "trace"))]
165pub async fn notification_count(&self, user_id: &UserId, room_id: &RoomId) -> u64 {
166 let key = (user_id, room_id);
167 self.db
168 .userroomid_notificationcount
169 .qry(&key)
170 .await
171 .deserialized()
172 .unwrap_or(0)
173}
174
175#[implement(super::Service)]
179#[tracing::instrument(level = "trace", skip(self), ret)]
180pub async fn global_notification_count(&self, user_id: &UserId) -> u64 {
181 self.db
182 .userroomid_notificationcount
183 .stream_prefix_raw(&(user_id, Interfix))
184 .ignore_err()
185 .ready_filter_map(|(key, count)| {
186 let count = u64_from_u8(count);
187
188 (count > 0).then(|| (KeyBuf::from(key), count))
189 })
190 .broad_filter_map(async |(key, count)| {
191 let (_, room_id, _): (Ignore, &RoomId, IgnoreAll) =
192 deserialize_key(&key).expect("notification count key");
193
194 self.services
195 .state_cache
196 .is_joined(user_id, room_id)
197 .await
198 .then_some(count)
199 })
200 .ready_fold(0_u64, u64::saturating_add)
201 .await
202}
203
204#[implement(super::Service)]
205#[tracing::instrument(level = "debug", skip(self), ret(level = "trace"))]
206pub async fn highlight_count(&self, user_id: &UserId, room_id: &RoomId) -> u64 {
207 let key = (user_id, room_id);
208 self.db
209 .userroomid_highlightcount
210 .qry(&key)
211 .await
212 .deserialized()
213 .unwrap_or(0)
214}
215
216#[implement(super::Service)]
220#[tracing::instrument(level = "debug", skip(self))]
221pub async fn thread_notification_counts(
222 &self,
223 user_id: &UserId,
224 room_id: &RoomId,
225) -> ThreadCounts {
226 let prefix = (user_id, room_id, Interfix);
227 let notifications = self
228 .db
229 .userroomid_notificationcount
230 .stream_prefix(&prefix)
231 .ignore_err()
232 .map(notification_kv);
233
234 let highlights = self
235 .db
236 .userroomid_highlightcount
237 .stream_prefix(&prefix)
238 .ignore_err()
239 .map(highlight_kv);
240
241 select(notifications, highlights)
242 .ready_fold(ThreadCounts::default(), merge_thread_count)
243 .await
244}
245
246fn notification_kv(
247 (key, notifications): ((&UserId, &RoomId, OwnedEventId), u64),
248) -> (OwnedEventId, (u64, u64)) {
249 (key.2, (notifications, 0))
250}
251
252fn highlight_kv(
253 (key, highlights): ((&UserId, &RoomId, OwnedEventId), u64),
254) -> (OwnedEventId, (u64, u64)) {
255 (key.2, (0, highlights))
256}
257
258fn merge_thread_count(
259 mut counts: ThreadCounts,
260 (root, (notifications, highlights)): (OwnedEventId, (u64, u64)),
261) -> ThreadCounts {
262 let entry = counts.entry(root).or_default();
263 entry.0 = entry.0.saturating_add(notifications);
264 entry.1 = entry.1.saturating_add(highlights);
265 counts
266}
267
268#[implement(super::Service)]
269#[tracing::instrument(level = "debug", skip(self), ret(level = "trace"))]
270pub async fn last_notification_read(&self, user_id: &UserId, room_id: &RoomId) -> Result<u64> {
271 let key = (room_id, user_id);
272 self.db
273 .roomuserid_lastnotificationread
274 .qry(&key)
275 .await
276 .deserialized()
277}
278
279#[implement(super::Service)]
283#[tracing::instrument(level = "debug", skip(self))]
284pub async fn thread_last_notification_reads(
285 &self,
286 user_id: &UserId,
287 room_id: &RoomId,
288) -> ThreadLastReads {
289 let prefix = (room_id, user_id, Interfix);
290 self.db
291 .roomuserid_lastnotificationread
292 .stream_prefix(&prefix)
293 .ignore_err()
294 .map(|((_, _, root), count): ((Ignore, Ignore, OwnedEventId), u64)| (root, count))
295 .collect()
296 .await
297}
298
299#[implement(super::Service)]
300pub async fn delete_room_notification_read(&self, room_id: &RoomId) -> Result {
301 let key = (room_id, Interfix);
302 self.db
303 .roomuserid_lastnotificationread
304 .keys_prefix_raw(&key)
305 .ignore_err()
306 .ready_for_each(|key| {
307 trace!("Removing key: {key:?}");
308 self.db
309 .roomuserid_lastnotificationread
310 .remove(key);
311 })
312 .await;
313
314 Ok(())
315}