multiverse/widgets/
room_list.rs

1use std::{collections::HashMap, sync::Arc};
2
3use imbl::Vector;
4use matrix_sdk::{locks::Mutex, ruma::OwnedRoomId};
5use matrix_sdk_ui::{room_list_service, sync_service::SyncService};
6use ratatui::{prelude::*, widgets::*};
7
8use crate::{
9    widgets::status::StatusHandle, UiRooms, ALT_ROW_COLOR, HEADER_BG, NORMAL_ROW_COLOR,
10    SELECTED_STYLE_FG, TEXT_COLOR,
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_list_service::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    /// Room list service rooms known to the app.
37    ui_rooms: UiRooms,
38
39    /// Extra information about rooms.
40    room_infos: RoomInfos,
41
42    /// The current room that's subscribed to in the room list's sliding sync.
43    current_room_subscription: Option<room_list_service::Room>,
44
45    /// The sync service used for synchronizing events.
46    sync_service: Arc<SyncService>,
47}
48
49impl RoomList {
50    pub fn new(
51        rooms: Rooms,
52        ui_rooms: UiRooms,
53        room_infos: RoomInfos,
54        sync_service: Arc<SyncService>,
55        status_handle: StatusHandle,
56    ) -> Self {
57        Self {
58            state: Default::default(),
59            rooms,
60            status_handle,
61            room_infos,
62            current_room_subscription: None,
63            ui_rooms,
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 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);
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 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);
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    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) = self
130            .get_room_id_of_entry(index)
131            .and_then(|room_id| self.ui_rooms.lock().get(&room_id).cloned())
132        {
133            self.sync_service.room_list_service().subscribe_to_rooms(&[room.room_id()]);
134            self.current_room_subscription = Some(room);
135        }
136    }
137}
138
139impl Widget for &mut RoomList {
140    fn render(self, area: Rect, buf: &mut Buffer)
141    where
142        Self: Sized,
143    {
144        // We create two blocks, one is for the header (outer) and the other is for list
145        // (inner).
146        let outer_block = Block::default()
147            .borders(Borders::RIGHT)
148            .border_set(symbols::border::THICK)
149            .fg(TEXT_COLOR)
150            .bg(HEADER_BG)
151            .title("Room list")
152            .title_alignment(Alignment::Center);
153        let inner_block =
154            Block::default().borders(Borders::NONE).fg(TEXT_COLOR).bg(NORMAL_ROW_COLOR);
155
156        // We get the inner area from outer_block. We'll use this area later to render
157        // the table.
158        let outer_area = area;
159        let inner_area = outer_block.inner(outer_area);
160
161        // We can render the header in outer_area.
162        outer_block.render(outer_area, buf);
163
164        // Don't keep this lock too long by cloning the content. RAM's free these days,
165        // right?
166        let mut room_info = self.room_infos.lock().clone();
167
168        // Iterate through all elements in the `items` and stylize them.
169        let items: Vec<ListItem<'_>> = self
170            .rooms
171            .lock()
172            .iter()
173            .enumerate()
174            .map(|(i, room)| {
175                let bg_color = match i % 2 {
176                    0 => NORMAL_ROW_COLOR,
177                    _ => ALT_ROW_COLOR,
178                };
179
180                let line = {
181                    let room_id = room.room_id();
182                    let room_info = room_info.remove(room_id);
183
184                    let (raw, display, is_dm) = if let Some(info) = room_info {
185                        (info.raw_name, info.display_name, info.is_dm)
186                    } else {
187                        (None, None, None)
188                    };
189
190                    let dm_marker = if is_dm.unwrap_or(false) { "🤫" } else { "" };
191
192                    let room_name = if let Some(n) = display {
193                        format!("{n} ({room_id})")
194                    } else if let Some(n) = raw {
195                        format!("m.room.name:{n} ({room_id})")
196                    } else {
197                        room_id.to_string()
198                    };
199
200                    format!("#{i}{dm_marker} {}", room_name)
201                };
202
203                let line = Line::styled(line, TEXT_COLOR);
204                ListItem::new(line).bg(bg_color)
205            })
206            .collect();
207
208        // Create a List from all list items and highlight the currently selected one.
209        let items = List::new(items)
210            .block(inner_block)
211            .highlight_style(
212                Style::default()
213                    .add_modifier(Modifier::BOLD)
214                    .add_modifier(Modifier::REVERSED)
215                    .fg(SELECTED_STYLE_FG),
216            )
217            .highlight_symbol(">")
218            .highlight_spacing(HighlightSpacing::Always);
219
220        StatefulWidget::render(items, inner_area, buf, &mut self.state);
221    }
222}