multiverse/widgets/
room_list.rs

1use std::{collections::HashMap, sync::Arc};
2
3use imbl::Vector;
4use matrix_sdk::{Client, Room, locks::Mutex, ruma::OwnedRoomId};
5use matrix_sdk_ui::sync_service::SyncService;
6use ratatui::{prelude::*, widgets::*};
7
8use crate::{
9    ALT_ROW_COLOR, HEADER_BG, NORMAL_ROW_COLOR, SELECTED_STYLE_FG, TEXT_COLOR,
10    widgets::status::StatusHandle,
11};
12
13/// Extra room information, like its display name, etc.
14#[derive(Clone)]
15pub struct ExtraRoomInfo {
16    /// Content of the raw m.room.name event, if available.
17    pub raw_name: Option<String>,
18
19    /// Calculated display name for the room.
20    pub display_name: Option<String>,
21
22    /// Is the room a DM?
23    pub is_dm: Option<bool>,
24}
25
26pub type Rooms = Arc<Mutex<Vector<Room>>>;
27pub type RoomInfos = Arc<Mutex<HashMap<OwnedRoomId, ExtraRoomInfo>>>;
28
29pub struct RoomList {
30    pub state: ListState,
31
32    pub status_handle: StatusHandle,
33
34    pub rooms: Rooms,
35
36    client: Client,
37
38    /// Extra information about rooms.
39    room_infos: RoomInfos,
40
41    /// The current room that's subscribed to in the room list's sliding sync.
42    current_room_subscription: Option<Room>,
43
44    /// The sync service used for synchronizing events.
45    sync_service: Arc<SyncService>,
46}
47
48impl RoomList {
49    pub fn new(
50        client: Client,
51        rooms: Rooms,
52
53        room_infos: RoomInfos,
54        sync_service: Arc<SyncService>,
55        status_handle: StatusHandle,
56    ) -> Self {
57        Self {
58            client,
59            state: Default::default(),
60            rooms,
61            status_handle,
62            room_infos,
63            current_room_subscription: None,
64            sync_service,
65        }
66    }
67
68    /// Focus the list on the next item, wraps around if needs be.
69    ///
70    /// Returns the index only if there was a meaningful change.
71    pub async fn next_room(&mut self) {
72        let num_items = self.rooms.lock().len();
73
74        // If there's no item to select, leave early.
75        if num_items == 0 {
76            self.state.select(None);
77            return;
78        }
79
80        // Otherwise, select the next one or wrap around.
81        let prev = self.state.selected();
82        let new = prev.map_or(0, |i| if i >= num_items - 1 { 0 } else { i + 1 });
83
84        if prev != Some(new) {
85            self.state.select(Some(new));
86            self.subscribe_to_room(new).await;
87        }
88    }
89
90    /// Focus the list on the previous item, wraps around if needs be.
91    ///
92    /// Returns the index only if there was a meaningful change.
93    pub async fn previous_room(&mut self) {
94        let num_items = self.rooms.lock().len();
95
96        // If there's no item to select, leave early.
97        if num_items == 0 {
98            self.state.select(None);
99            return;
100        }
101
102        // Otherwise, select the previous one or wrap around.
103        let prev = self.state.selected();
104        let new = prev.map_or(0, |i| if i == 0 { num_items - 1 } else { i - 1 });
105
106        if prev != Some(new) {
107            self.state.select(Some(new));
108            self.subscribe_to_room(new).await;
109        }
110    }
111
112    /// Returns the [`OwnedRoomId`] of the `nth` room within the [`RoomList`].
113    pub fn get_room_id_of_entry(&self, nth: usize) -> Option<OwnedRoomId> {
114        self.rooms.lock().get(nth).cloned().map(|room| room.room_id().to_owned())
115    }
116
117    /// Returns the [`OwnedRoomId`] of the currently selected room, if any.
118    pub fn get_selected_room_id(&self) -> Option<OwnedRoomId> {
119        let selected = self.state.selected()?;
120        self.get_room_id_of_entry(selected)
121    }
122
123    /// Subscribe to room that is shown at the given `index`.
124    async fn subscribe_to_room(&mut self, index: usize) {
125        // Cancel the subscription to the previous room, if any.
126        self.current_room_subscription.take();
127
128        // Subscribe to the new room.
129        if let Some(room) =
130            self.get_room_id_of_entry(index).and_then(|room_id| self.client.get_room(&room_id))
131        {
132            self.sync_service.room_list_service().subscribe_to_rooms(&[room.room_id()]).await;
133            self.current_room_subscription = Some(room);
134        }
135    }
136}
137
138impl Widget for &mut RoomList {
139    fn render(self, area: Rect, buf: &mut Buffer)
140    where
141        Self: Sized,
142    {
143        // We create two blocks, one is for the header (outer) and the other is for list
144        // (inner).
145        let outer_block = Block::default()
146            .borders(Borders::RIGHT)
147            .border_set(symbols::border::THICK)
148            .fg(TEXT_COLOR)
149            .bg(HEADER_BG)
150            .title("Room list")
151            .title_alignment(Alignment::Center);
152        let inner_block =
153            Block::default().borders(Borders::NONE).fg(TEXT_COLOR).bg(NORMAL_ROW_COLOR);
154
155        // We get the inner area from outer_block. We'll use this area later to render
156        // the table.
157        let outer_area = area;
158        let inner_area = outer_block.inner(outer_area);
159
160        // We can render the header in outer_area.
161        outer_block.render(outer_area, buf);
162
163        // Don't keep this lock too long by cloning the content. RAM's free these days,
164        // right?
165        let mut room_info = self.room_infos.lock().clone();
166
167        // Iterate through all elements in the `items` and stylize them.
168        let items: Vec<ListItem<'_>> = self
169            .rooms
170            .lock()
171            .iter()
172            .enumerate()
173            .map(|(i, room)| {
174                let bg_color = match i % 2 {
175                    0 => NORMAL_ROW_COLOR,
176                    _ => ALT_ROW_COLOR,
177                };
178
179                let line = {
180                    let room_id = room.room_id();
181                    let room_info = room_info.remove(room_id);
182
183                    let (raw, display, is_dm) = if let Some(info) = room_info {
184                        (info.raw_name, info.display_name, info.is_dm)
185                    } else {
186                        (None, None, None)
187                    };
188
189                    let dm_marker = if is_dm.unwrap_or(false) { "🤫" } else { "" };
190
191                    let room_name = if let Some(n) = display {
192                        format!("{n} ({room_id})")
193                    } else if let Some(n) = raw {
194                        format!("m.room.name:{n} ({room_id})")
195                    } else {
196                        room_id.to_string()
197                    };
198
199                    format!("#{i}{dm_marker} {room_name}")
200                };
201
202                let line = Line::styled(line, TEXT_COLOR);
203                ListItem::new(line).bg(bg_color)
204            })
205            .collect();
206
207        // Create a List from all list items and highlight the currently selected one.
208        let items = List::new(items)
209            .block(inner_block)
210            .highlight_style(
211                Style::default()
212                    .add_modifier(Modifier::BOLD)
213                    .add_modifier(Modifier::REVERSED)
214                    .fg(SELECTED_STYLE_FG),
215            )
216            .highlight_symbol(">")
217            .highlight_spacing(HighlightSpacing::Always);
218
219        StatefulWidget::render(items, inner_area, buf, &mut self.state);
220    }
221}