1use std::{collections::HashMap, sync::Arc};
23use imbl::Vector;
4use matrix_sdk::{locks::Mutex, ruma::OwnedRoomId};
5use matrix_sdk_ui::{room_list_service, sync_service::SyncService};
6use ratatui::{prelude::*, widgets::*};
78use crate::{
9 widgets::status::StatusHandle, UiRooms, ALT_ROW_COLOR, HEADER_BG, NORMAL_ROW_COLOR,
10 SELECTED_STYLE_FG, TEXT_COLOR,
11};
1213/// 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.
17pub raw_name: Option<String>,
1819/// Calculated display name for the room.
20pub display_name: Option<String>,
2122/// Is the room a DM?
23pub is_dm: Option<bool>,
24}
2526pub type Rooms = Arc<Mutex<Vector<room_list_service::Room>>>;
27pub type RoomInfos = Arc<Mutex<HashMap<OwnedRoomId, ExtraRoomInfo>>>;
2829pub struct RoomList {
30pub state: ListState,
3132pub status_handle: StatusHandle,
3334pub rooms: Rooms,
3536/// Room list service rooms known to the app.
37ui_rooms: UiRooms,
3839/// Extra information about rooms.
40room_infos: RoomInfos,
4142/// The current room that's subscribed to in the room list's sliding sync.
43current_room_subscription: Option<room_list_service::Room>,
4445/// The sync service used for synchronizing events.
46sync_service: Arc<SyncService>,
47}
4849impl RoomList {
50pub fn new(
51 rooms: Rooms,
52 ui_rooms: UiRooms,
53 room_infos: RoomInfos,
54 sync_service: Arc<SyncService>,
55 status_handle: StatusHandle,
56 ) -> Self {
57Self {
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 }
6768/// Focus the list on the next item, wraps around if needs be.
69 ///
70 /// Returns the index only if there was a meaningful change.
71pub fn next_room(&mut self) {
72let num_items = self.rooms.lock().len();
7374// If there's no item to select, leave early.
75if num_items == 0 {
76self.state.select(None);
77return;
78 }
7980// Otherwise, select the next one or wrap around.
81let prev = self.state.selected();
82let new = prev.map_or(0, |i| if i >= num_items - 1 { 0 } else { i + 1 });
8384if prev != Some(new) {
85self.state.select(Some(new));
86self.subscribe_to_room(new);
87 }
88 }
8990/// Focus the list on the previous item, wraps around if needs be.
91 ///
92 /// Returns the index only if there was a meaningful change.
93pub fn previous_room(&mut self) {
94let num_items = self.rooms.lock().len();
9596// If there's no item to select, leave early.
97if num_items == 0 {
98self.state.select(None);
99return;
100 }
101102// Otherwise, select the previous one or wrap around.
103let prev = self.state.selected();
104let new = prev.map_or(0, |i| if i == 0 { num_items - 1 } else { i - 1 });
105106if prev != Some(new) {
107self.state.select(Some(new));
108self.subscribe_to_room(new);
109 }
110 }
111112/// Returns the [`OwnedRoomId`] of the `nth` room within the [`RoomList`].
113pub fn get_room_id_of_entry(&self, nth: usize) -> Option<OwnedRoomId> {
114self.rooms.lock().get(nth).cloned().map(|room| room.room_id().to_owned())
115 }
116117/// Returns the [`OwnedRoomId`] of the currently selected room, if any.
118pub fn get_selected_room_id(&self) -> Option<OwnedRoomId> {
119let selected = self.state.selected()?;
120self.get_room_id_of_entry(selected)
121 }
122123/// Subscribe to room that is shown at the given `index`.
124fn subscribe_to_room(&mut self, index: usize) {
125// Cancel the subscription to the previous room, if any.
126self.current_room_subscription.take();
127128// Subscribe to the new room.
129if 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 {
133self.sync_service.room_list_service().subscribe_to_rooms(&[room.room_id()]);
134self.current_room_subscription = Some(room);
135 }
136 }
137}
138139impl Widget for &mut RoomList {
140fn render(self, area: Rect, buf: &mut Buffer)
141where
142Self: Sized,
143 {
144// We create two blocks, one is for the header (outer) and the other is for list
145 // (inner).
146let 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);
153let inner_block =
154 Block::default().borders(Borders::NONE).fg(TEXT_COLOR).bg(NORMAL_ROW_COLOR);
155156// We get the inner area from outer_block. We'll use this area later to render
157 // the table.
158let outer_area = area;
159let inner_area = outer_block.inner(outer_area);
160161// We can render the header in outer_area.
162outer_block.render(outer_area, buf);
163164// Don't keep this lock too long by cloning the content. RAM's free these days,
165 // right?
166let mut room_info = self.room_infos.lock().clone();
167168// Iterate through all elements in the `items` and stylize them.
169let items: Vec<ListItem<'_>> = self
170.rooms
171 .lock()
172 .iter()
173 .enumerate()
174 .map(|(i, room)| {
175let bg_color = match i % 2 {
1760 => NORMAL_ROW_COLOR,
177_ => ALT_ROW_COLOR,
178 };
179180let line = {
181let room_id = room.room_id();
182let room_info = room_info.remove(room_id);
183184let (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 };
189190let dm_marker = if is_dm.unwrap_or(false) { "🤫" } else { "" };
191192let room_name = if let Some(n) = display {
193format!("{n} ({room_id})")
194 } else if let Some(n) = raw {
195format!("m.room.name:{n} ({room_id})")
196 } else {
197 room_id.to_string()
198 };
199200format!("#{i}{dm_marker} {}", room_name)
201 };
202203let line = Line::styled(line, TEXT_COLOR);
204 ListItem::new(line).bg(bg_color)
205 })
206 .collect();
207208// Create a List from all list items and highlight the currently selected one.
209let 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);
219220 StatefulWidget::render(items, inner_area, buf, &mut self.state);
221 }
222}