tauri_plugin_matrix_svelte/matrix/
rooms.rs

1use std::sync::Arc;
2
3use crate::matrix::{
4    events::get_latest_event_details,
5    room::rooms_list::{enqueue_rooms_list_update, JoinedRoomInfo, RoomsListUpdate},
6    singletons::{ALL_JOINED_ROOMS, TOMBSTONED_ROOMS},
7    timeline::{timeline_subscriber_handler, update_latest_event},
8};
9use anyhow::bail;
10use matrix_sdk::{
11    event_handler::EventHandlerDropGuard, ruma::OwnedRoomId, RoomDisplayName, RoomState,
12};
13use matrix_sdk_ui::{room_list_service, RoomListService, Timeline};
14use serde::Serialize;
15use tokio::{runtime::Handle, sync::watch, task::JoinHandle};
16
17use super::{
18    singletons::LOG_ROOM_LIST_DIFFS,
19    timeline::{TimelineRequestSender, TimelineUpdate},
20};
21
22/// Backend-specific details about a joined room that our client currently knows about.
23pub struct JoinedRoomDetails {
24    #[allow(unused)]
25    room_id: OwnedRoomId,
26    /// A reference to this room's timeline of events.
27    pub timeline: Arc<Timeline>,
28    /// An instance of the clone-able sender that can be used to send updates to this room's timeline.
29    pub timeline_update_sender: crossbeam_channel::Sender<TimelineUpdate>,
30    /// A tuple of two separate channel endpoints that can only be taken *once* by the main UI thread.
31    ///
32    /// 1. The single receiver that can receive updates to this room's timeline.
33    ///    * When a new room is joined, an unbounded crossbeam channel will be created
34    ///      and its sender given to a background task (the `timeline_subscriber_handler()`)
35    ///      that enqueues timeline updates as it receives timeline vector diffs from the server.
36    ///    * The UI thread can take ownership of this update receiver in order to receive updates
37    ///      to this room's timeline, but only one receiver can exist at a time.
38    /// 2. The sender that can send requests to the background timeline subscriber handler,
39    ///    e.g., to watch for a specific event to be prepended to the timeline (via back pagination).
40    pub timeline_singleton_endpoints: Option<(
41        crossbeam_channel::Receiver<TimelineUpdate>,
42        TimelineRequestSender,
43    )>,
44    /// The async task that listens for timeline updates for this room and sends them to the UI thread.
45    timeline_subscriber_handler_task: JoinHandle<()>,
46    /// A drop guard for the event handler that represents a subscription to typing notices for this room.
47    pub typing_notice_subscriber: Option<EventHandlerDropGuard>,
48    /// The ID of the old tombstoned room that this room has replaced, if any.
49    replaces_tombstoned_room: Option<OwnedRoomId>,
50}
51impl Drop for JoinedRoomDetails {
52    // TODO: implement dropping the Svelte store as well
53    fn drop(&mut self) {
54        println!("Dropping RoomInfo for room {}", self.room_id);
55        self.timeline_subscriber_handler_task.abort();
56        drop(self.typing_notice_subscriber.take());
57        if let Some(replaces_tombstoned_room) = self.replaces_tombstoned_room.take() {
58            TOMBSTONED_ROOMS
59                .lock()
60                .unwrap()
61                .insert(self.room_id.clone(), replaces_tombstoned_room);
62        }
63    }
64}
65
66/// Info we store about a room received by the room list service.
67///
68/// This struct is necessary in order for us to track the previous state
69/// of a room received from the room list service, so that we can
70/// determine if the room has changed state.
71/// We can't just store the `room_list_service::Room` object itself,
72/// because that is a shallow reference to an inner room object within
73/// the room list service
74#[derive(Clone)]
75pub struct RoomListServiceRoomInfo {
76    room: room_list_service::Room,
77    pub room_id: OwnedRoomId,
78    room_state: RoomState,
79}
80impl From<&room_list_service::Room> for RoomListServiceRoomInfo {
81    fn from(room: &room_list_service::Room) -> Self {
82        room.clone().into()
83    }
84}
85impl From<room_list_service::Room> for RoomListServiceRoomInfo {
86    fn from(room: room_list_service::Room) -> Self {
87        Self {
88            room_id: room.room_id().to_owned(),
89            room_state: room.state(),
90            room,
91        }
92    }
93}
94
95/// The number of unread messages in a room.
96#[derive(Clone, Debug)]
97pub enum UnreadMessageCount {
98    /// There are unread messages, but we do not know how many.
99    Unknown,
100    /// There are unread messages, and we know exactly how many.
101    Known(u64),
102}
103
104#[derive(Debug, Clone, Serialize)]
105#[serde(rename_all = "camelCase")]
106pub struct FrontendRoom {
107    id: String,
108    name: RoomDisplayName,
109    avatar: Option<Vec<u8>>,
110    highlight_count: u64,
111    notification_count: u64,
112    latest_message: String,
113}
114
115pub async fn add_new_room(
116    room: &room_list_service::Room,
117    room_list_service: &RoomListService,
118) -> anyhow::Result<()> {
119    let room_id = room.room_id().to_owned();
120    // We must call `display_name()` here to calculate and cache the room's name.
121    let room_name = room.display_name().await.map(|n| n.to_string()).ok();
122
123    match room.state() {
124        RoomState::Knocked => {
125            // TODO: handle Knocked rooms (e.g., can you re-knock? or cancel a prior knock?)
126            return Ok(());
127        }
128        RoomState::Banned => {
129            println!("Got new Banned room: {room_name:?} ({room_id})");
130            // TODO: handle rooms that this user has been banned from.
131            return Ok(());
132        }
133        RoomState::Left => {
134            println!("Got new Left room: {room_name:?} ({room_id})");
135            // TODO: add this to the list of left rooms,
136            //       which is collapsed by default.
137            //       Upon clicking a left room, we can show a splash page
138            //       that prompts the user to rejoin the room or forget it.
139
140            // TODO: this may also be called when a user rejects an invite, not sure.
141            //       So we might also need to make a new RoomsListUpdate::RoomLeft variant.
142            return Ok(());
143        }
144        // RoomState::Invited => {
145        //     let invite_details = room.invite_details().await.ok();
146        //     let latest = room
147        //         .latest_event()
148        //         .await
149        //         .as_ref()
150        //         .map(|ev| get_latest_event_details(ev, &room_id));
151        //     let room_avatar = room_avatar(room, room_name.as_deref()).await;
152
153        //     let inviter_info = if let Some(inviter) = invite_details.and_then(|d| d.inviter) {
154        //         Some(InviterInfo {
155        //             user_id: inviter.user_id().to_owned(),
156        //             display_name: inviter.display_name().map(|n| n.to_string()),
157        //             avatar: inviter
158        //                 .avatar(AVATAR_THUMBNAIL_FORMAT.into())
159        //                 .await
160        //                 .ok()
161        //                 .flatten()
162        //                 .map(Into::into),
163        //         })
164        //     } else {
165        //         None
166        //     };
167
168        //     rooms_list::enqueue_rooms_list_update(RoomsListUpdate::AddInvitedRoom(
169        //         InvitedRoomInfo {
170        //             room_id,
171        //             room_name,
172        //             inviter_info,
173        //             room_avatar,
174        //             canonical_alias: room.canonical_alias(),
175        //             alt_aliases: room.alt_aliases(),
176        //             latest,
177        //             invite_state: Default::default(),
178        //             is_selected: false,
179        //         },
180        //     ));
181        //     return Ok(());
182        // }
183        RoomState::Joined => {} // Fall through to adding the joined room below.
184        _ => bail!("We do not handle invited rooms yet"),
185    }
186
187    // Subscribe to all updates for this room in order to properly receive all of its states.
188    room_list_service.subscribe_to_rooms(&[&room_id]);
189
190    // Do not add tombstoned rooms to the rooms list; they require special handling.
191    if let Some(tombstoned_info) = room.tombstone() {
192        println!("Room {room_id} has been tombstoned: {tombstoned_info:#?}");
193        // Since we don't know the order in which we'll learn about new rooms,
194        // we need to first check to see if the replacement for this tombstoned room
195        // refers to an already-known room as its replacement.
196        // If so, we can immediately update the replacement room's room info
197        // to indicate that it replaces this tombstoned room.
198        let replacement_room_id = tombstoned_info.replacement_room;
199        if let Some(room_info) = ALL_JOINED_ROOMS
200            .lock()
201            .unwrap()
202            .get_mut(&replacement_room_id)
203        {
204            room_info.replaces_tombstoned_room = Some(replacement_room_id.clone());
205        }
206        // But if we don't know about the replacement room yet, we need to save this tombstoned room
207        // in a separate list so that the replacement room we will discover in the future
208        // can know which old tombstoned room it replaces (see the bottom of this function).
209        else {
210            TOMBSTONED_ROOMS
211                .lock()
212                .unwrap()
213                .insert(replacement_room_id, room_id.clone());
214        }
215        return Ok(());
216    }
217
218    let timeline = if let Some(tl_arc) = room.timeline() {
219        tl_arc
220    } else {
221        let builder = room
222            .default_room_timeline_builder()
223            .await?
224            .track_read_marker_and_receipts();
225        room.init_timeline_with_builder(builder).await?;
226        room.timeline()
227            .ok_or_else(|| anyhow::anyhow!("BUG: room timeline not found for room {room_id}"))?
228    };
229    let latest_event = timeline.latest_event().await;
230    let (timeline_update_sender, timeline_update_receiver) = crossbeam_channel::unbounded();
231
232    let (request_sender, request_receiver) = watch::channel(Vec::new());
233    let timeline_subscriber_handler_task = Handle::current().spawn(timeline_subscriber_handler(
234        room.inner_room().clone(),
235        timeline.clone(),
236        timeline_update_sender.clone(),
237        request_receiver,
238    ));
239
240    let latest = latest_event
241        .as_ref()
242        .map(|ev| get_latest_event_details(ev, &room_id));
243
244    let tombstoned_room_replaced_by_this_room = TOMBSTONED_ROOMS.lock().unwrap().remove(&room_id);
245
246    println!("Adding new joined room {room_id}. Replaces tombstoned room: {tombstoned_room_replaced_by_this_room:?}");
247    ALL_JOINED_ROOMS.lock().unwrap().insert(
248        room_id.clone(),
249        JoinedRoomDetails {
250            room_id: room_id.clone(),
251            timeline,
252            timeline_singleton_endpoints: Some((timeline_update_receiver, request_sender)),
253            timeline_update_sender,
254            timeline_subscriber_handler_task,
255            typing_notice_subscriber: None,
256            replaces_tombstoned_room: tombstoned_room_replaced_by_this_room,
257        },
258    );
259
260    // We need to add the room to the `ALL_JOINED_ROOMS` list before we can
261    // send the `AddJoinedRoom` update to the UI, because the UI might immediately
262    // issue a `MatrixRequest` that relies on that room being in `ALL_JOINED_ROOMS`.
263    enqueue_rooms_list_update(RoomsListUpdate::AddJoinedRoom(JoinedRoomInfo {
264        room_id,
265        latest,
266        tags: room.tags().await.ok().flatten().unwrap_or_default(),
267        num_unread_messages: room.num_unread_messages(),
268        num_unread_mentions: room.num_unread_mentions(),
269        // start with a basic text avatar; the avatar image will be fetched asynchronously below.
270        // avatar: avatar_from_room_name(room_name.as_deref()),
271        room_name,
272        canonical_alias: room.canonical_alias(),
273        alt_aliases: room.alt_aliases(),
274        has_been_paginated: false,
275        is_selected: false,
276    }));
277
278    // spawn_fetch_room_avatar(room.inner_room().clone());
279
280    Ok(())
281}
282
283/// Invoked when the room list service has received an update that changes an existing room.
284pub async fn update_room(
285    old_room: &RoomListServiceRoomInfo,
286    new_room: &room_list_service::Room,
287    room_list_service: &RoomListService,
288) -> anyhow::Result<()> {
289    let new_room_id = new_room.room_id().to_owned();
290    if old_room.room_id == new_room_id {
291        let new_room_name = new_room.display_name().await.map(|n| n.to_string()).ok();
292        let mut room_avatar_changed = false;
293
294        // Handle state transitions for a room.
295        let old_room_state = old_room.room_state;
296        let new_room_state = new_room.state();
297        if old_room_state != new_room_state {
298            if LOG_ROOM_LIST_DIFFS {
299                println!("Room {new_room_name:?} ({new_room_id}) changed from {old_room_state:?} to {new_room_state:?}");
300            }
301            match new_room_state {
302                RoomState::Banned => {
303                    // TODO: handle rooms that this user has been banned from.
304                    println!("Removing Banned room: {new_room_name:?} ({new_room_id})");
305                    remove_room(&new_room.into());
306                    return Ok(());
307                }
308                RoomState::Left => {
309                    println!("Removing Left room: {new_room_name:?} ({new_room_id})");
310                    remove_room(&new_room.into());
311                    // TODO: we could add this to the list of left rooms,
312                    //       which is collapsed by default.
313                    //       Upon clicking a left room, we can show a splash page
314                    //       that prompts the user to rejoin the room or forget it.
315
316                    // TODO: this may also be called when a user rejects an invite, not sure.
317                    //       So we might also need to make a new RoomsListUpdate::RoomLeft variant.
318                    return Ok(());
319                }
320                RoomState::Joined => {
321                    println!(
322                        "update_room(): adding new Joined room: {new_room_name:?} ({new_room_id})"
323                    );
324                    return add_new_room(new_room, room_list_service).await;
325                }
326                RoomState::Invited => {
327                    println!(
328                        "update_room(): adding new Invited room: {new_room_name:?} ({new_room_id})"
329                    );
330                    return add_new_room(new_room, room_list_service).await;
331                }
332                RoomState::Knocked => {
333                    // TODO: handle Knocked rooms (e.g., can you re-knock? or cancel a prior knock?)
334                    return Ok(());
335                }
336            }
337        }
338
339        if let Some(new_latest_event) = new_room.latest_event().await {
340            if let Some(old_latest_event) = old_room.room.latest_event().await {
341                if new_latest_event.timestamp() > old_latest_event.timestamp() {
342                    println!("Updating latest event for room {}", new_room_id);
343                    room_avatar_changed =
344                        update_latest_event(new_room_id.clone(), &new_latest_event, None);
345                }
346            }
347        }
348
349        if room_avatar_changed || (old_room.room.avatar_url() != new_room.avatar_url()) {
350            println!("Updating avatar for room {}", new_room_id);
351            // spawn_fetch_room_avatar(new_room.inner_room().clone());
352        }
353
354        if let Some(new_room_name) = new_room_name {
355            if old_room.room.cached_display_name().as_ref() != Some(&new_room_name) {
356                println!(
357                    "Updating room name for room {} to {}",
358                    new_room_id, new_room_name
359                );
360                enqueue_rooms_list_update(RoomsListUpdate::UpdateRoomName {
361                    room_id: new_room_id.clone(),
362                    new_room_name,
363                });
364            }
365        }
366
367        if let Ok(new_tags) = new_room.tags().await {
368            enqueue_rooms_list_update(RoomsListUpdate::Tags {
369                room_id: new_room_id.clone(),
370                new_tags: new_tags.unwrap_or_default(),
371            });
372        }
373
374        enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages {
375            room_id: new_room_id.clone(),
376            count: UnreadMessageCount::Known(new_room.num_unread_messages()),
377            unread_mentions: new_room.num_unread_mentions(),
378        });
379
380        Ok(())
381    } else {
382        println!(
383            "UNTESTED SCENARIO: update_room(): removing old room {}, replacing with new room {}",
384            old_room.room_id, new_room_id,
385        );
386        remove_room(old_room);
387        add_new_room(new_room, room_list_service).await
388    }
389}
390
391/// Invoked when the room list service has received an update to remove an existing room.
392pub fn remove_room(room: &RoomListServiceRoomInfo) {
393    ALL_JOINED_ROOMS.lock().unwrap().remove(&room.room_id);
394    enqueue_rooms_list_update(RoomsListUpdate::RemoveRoom {
395        room_id: room.room_id.clone(),
396        new_state: room.room_state,
397    });
398}