multiverse/widgets/room_view/
mod.rs
1use std::{ops::Deref, sync::Arc};
2
3use color_eyre::Result;
4use crossterm::event::{Event, KeyCode, KeyModifiers};
5use input::MessageOrCommand;
6use invited_room::InvitedRoomView;
7use matrix_sdk::{
8 locks::Mutex,
9 ruma::{
10 api::client::receipt::create_receipt::v3::ReceiptType,
11 events::room::message::RoomMessageEventContent, OwnedRoomId, OwnedUserId,
12 },
13 RoomState,
14};
15use ratatui::{prelude::*, widgets::*};
16use tokio::{spawn, task::JoinHandle};
17
18use self::{details::RoomDetails, input::Input, timeline::TimelineView};
19use super::status::StatusHandle;
20use crate::{
21 widgets::recovery::ShouldExit, Timelines, UiRooms, HEADER_BG, NORMAL_ROW_COLOR, TEXT_COLOR,
22};
23
24mod details;
25mod input;
26mod invited_room;
27mod timeline;
28
29const DEFAULT_TILING_DIRECTION: Direction = Direction::Horizontal;
30
31enum Mode {
32 Normal { invited_room_view: Option<InvitedRoomView> },
33 Details { tiling_direction: Direction, view: RoomDetails },
34}
35
36pub struct RoomView {
37 selected_room: Option<OwnedRoomId>,
38
39 ui_rooms: UiRooms,
41
42 timelines: Timelines,
44
45 status_handle: StatusHandle,
46
47 current_pagination: Arc<Mutex<Option<JoinHandle<()>>>>,
48
49 mode: Mode,
50
51 input: Input,
52}
53
54impl RoomView {
55 pub fn new(ui_rooms: UiRooms, timelines: Timelines, status_handle: StatusHandle) -> Self {
56 Self {
57 selected_room: None,
58 ui_rooms,
59 timelines,
60 status_handle,
61 current_pagination: Default::default(),
62 mode: Mode::Normal { invited_room_view: None },
63 input: Input::new(),
64 }
65 }
66
67 pub async fn handle_event(&mut self, event: Event) {
68 use KeyCode::*;
69
70 match &mut self.mode {
71 Mode::Normal { invited_room_view } => {
72 if let Some(view) = invited_room_view {
73 view.handle_event(event);
74 } else if let Event::Key(key) = event {
75 match (key.modifiers, key.code) {
76 (KeyModifiers::NONE, Enter) => {
77 if !self.input.is_empty() {
78 let message_or_command = self.input.get_input();
79
80 match message_or_command {
81 Ok(MessageOrCommand::Message(message)) => {
82 self.send_message(message).await
83 }
84 Ok(MessageOrCommand::Command(command)) => {
85 self.handle_command(command).await
86 }
87 Err(e) => {
88 self.status_handle.set_message(e.render().to_string());
89 self.input.clear();
90 }
91 }
92 }
93 }
94
95 (KeyModifiers::CONTROL, Char('l')) => {
96 self.toggle_reaction_to_latest_msg().await
97 }
98
99 (KeyModifiers::NONE, PageUp) => self.back_paginate(),
100
101 (KeyModifiers::ALT, Char('e')) => {
102 if self.selected_room.is_some() {
103 self.mode = Mode::Details {
104 tiling_direction: DEFAULT_TILING_DIRECTION,
105 view: RoomDetails::with_events_as_selected(),
106 }
107 }
108 }
109
110 (KeyModifiers::ALT, Char('r')) => {
111 if self.selected_room.is_some() {
112 self.mode = Mode::Details {
113 tiling_direction: DEFAULT_TILING_DIRECTION,
114 view: RoomDetails::with_receipts_as_selected(),
115 }
116 }
117 }
118
119 (KeyModifiers::ALT, Char('l')) => {
120 if self.selected_room.is_some() {
121 self.mode = Mode::Details {
122 tiling_direction: DEFAULT_TILING_DIRECTION,
123 view: RoomDetails::with_chunks_as_selected(),
124 }
125 }
126 }
127
128 _ => self.input.handle_key_press(key),
129 }
130 }
131 }
132
133 Mode::Details { view, tiling_direction } => {
134 if let Event::Key(key) = event {
135 match (key.modifiers, key.code) {
136 (KeyModifiers::NONE, PageUp) => self.back_paginate(),
137
138 (KeyModifiers::ALT, Char('t')) => {
139 let new_layout = match tiling_direction {
140 Direction::Horizontal => Direction::Vertical,
141 Direction::Vertical => Direction::Horizontal,
142 };
143
144 *tiling_direction = new_layout;
145 }
146
147 (KeyModifiers::ALT, Char('e')) => {
148 self.mode = Mode::Details {
149 tiling_direction: *tiling_direction,
150 view: RoomDetails::with_events_as_selected(),
151 }
152 }
153
154 (KeyModifiers::ALT, Char('r')) => {
155 self.mode = Mode::Details {
156 tiling_direction: *tiling_direction,
157 view: RoomDetails::with_receipts_as_selected(),
158 }
159 }
160
161 (KeyModifiers::ALT, Char('l')) => {
162 self.mode = Mode::Details {
163 tiling_direction: *tiling_direction,
164 view: RoomDetails::with_chunks_as_selected(),
165 }
166 }
167
168 _ => match view.handle_key_press(key) {
169 ShouldExit::No => {}
170 ShouldExit::OnlySubScreen => {}
171 ShouldExit::Yes => self.mode = Mode::Normal { invited_room_view: None },
172 },
173 }
174 }
175 }
176 }
177 }
178
179 pub fn set_selected_room(&mut self, room: Option<OwnedRoomId>) {
180 if let Some(room_id) = room.as_deref() {
181 let rooms = self.ui_rooms.lock();
182 let maybe_room = rooms.get(room_id);
183
184 if let Some(room) = maybe_room {
185 if matches!(room.state(), RoomState::Invited) {
186 let room = room.clone();
187 let view = InvitedRoomView::new(room);
188 self.mode = Mode::Normal { invited_room_view: Some(view) }
189 } else {
190 match &mut self.mode {
191 Mode::Normal { invited_room_view } => {
192 invited_room_view.take();
193 }
194 Mode::Details { .. } => {}
195 }
196 }
197 }
198 }
199
200 self.selected_room = room;
201 }
202
203 pub fn back_paginate(&mut self) {
206 let Some(sdk_timeline) = self.selected_room.as_deref().and_then(|room_id| {
207 self.timelines.lock().get(room_id).map(|timeline| timeline.timeline.clone())
208 }) else {
209 self.status_handle.set_message("missing timeline for room".to_owned());
210 return;
211 };
212
213 let mut pagination = self.current_pagination.lock();
214
215 if let Some(prev) = pagination.take() {
217 prev.abort();
218 }
219
220 let status_handle = self.status_handle.clone();
221
222 *pagination = Some(spawn(async move {
225 if let Err(err) = sdk_timeline.paginate_backwards(20).await {
226 status_handle.set_message(format!("Error during backpagination: {err}"));
227 }
228 }));
229 }
230
231 pub async fn toggle_reaction_to_latest_msg(&mut self) {
232 let selected = self.selected_room.as_deref();
233
234 if let Some((sdk_timeline, items)) = selected.and_then(|room_id| {
235 self.timelines
236 .lock()
237 .get(room_id)
238 .map(|timeline| (timeline.timeline.clone(), timeline.items.clone()))
239 }) {
240 let item_id = {
242 let items = items.lock();
243 items.iter().rev().find_map(|it| {
244 it.as_event()
245 .and_then(|ev| ev.content().as_message().is_some().then(|| ev.identifier()))
246 })
247 };
248
249 if let Some(item_id) = item_id {
251 match sdk_timeline.toggle_reaction(&item_id, "🥰").await {
252 Ok(_) => {
253 self.status_handle.set_message("reaction sent!".to_owned());
254 }
255 Err(err) => {
256 self.status_handle.set_message(format!("error when reacting: {err}"))
257 }
258 }
259 } else {
260 self.status_handle.set_message("no item to react to".to_owned());
261 }
262 } else {
263 self.status_handle.set_message("missing timeline for room".to_owned());
264 };
265 }
266
267 async fn invite_member(&mut self, user_id: OwnedUserId) {
268 let Some(room) = self
269 .selected_room
270 .as_deref()
271 .and_then(|room_id| self.ui_rooms.lock().get(room_id).cloned())
272 else {
273 self.status_handle
274 .set_message(format!("Coulnd't find the room object to invite {user_id}"));
275 return;
276 };
277
278 match room.invite_user_by_id(&user_id).await {
279 Ok(_) => {
280 self.status_handle
281 .set_message(format!("Successfully invited {user_id} to the room"));
282 self.input.clear();
283 }
284 Err(e) => {
285 self.status_handle
286 .set_message(format!("Failed to invite {user_id} to the room: {e:?}"));
287 }
288 }
289 }
290
291 async fn handle_command(&mut self, command: input::Command) {
292 match command {
293 input::Command::Invite { user_id } => self.invite_member(user_id).await,
294 }
295 }
296
297 async fn send_message(&mut self, message: String) {
298 match self.send_message_impl(message).await {
299 Ok(_) => {
300 self.input.clear();
301 }
302 Err(err) => {
303 self.status_handle.set_message(format!("error when sending event: {err}"));
304 }
305 }
306 }
307
308 async fn send_message_impl(&self, message: String) -> Result<()> {
309 if let Some(sdk_timeline) = self.selected_room.as_deref().and_then(|room_id| {
310 self.timelines.lock().get(room_id).map(|timeline| timeline.timeline.clone())
311 }) {
312 sdk_timeline.send(RoomMessageEventContent::text_plain(message).into()).await?;
313 } else {
314 self.status_handle.set_message("missing timeline for room".to_owned());
315 };
316
317 Ok(())
318 }
319
320 pub async fn mark_as_read(&mut self) {
322 let selected = self.selected_room.as_deref();
323
324 let Some(room) = selected.and_then(|room_id| self.ui_rooms.lock().get(room_id).cloned())
325 else {
326 self.status_handle.set_message("missing room or nothing to show".to_owned());
327 return;
328 };
329
330 match room.timeline().unwrap().mark_as_read(ReceiptType::Read).await {
332 Ok(did) => {
333 self.status_handle.set_message(format!(
334 "did {}send a read receipt!",
335 if did { "" } else { "not " }
336 ));
337 }
338 Err(err) => {
339 self.status_handle
340 .set_message(format!("error when marking a room as read: {err}",));
341 }
342 }
343 }
344
345 fn update(&mut self) {
346 match &mut self.mode {
347 Mode::Normal { invited_room_view } => {
348 if invited_room_view.as_ref().is_some_and(|view| view.should_switch()) {
349 self.mode = Mode::Normal { invited_room_view: None };
350 }
351 }
352 Mode::Details { .. } => {}
353 }
354 }
355}
356
357impl Widget for &mut RoomView {
358 fn render(self, area: Rect, buf: &mut Buffer)
359 where
360 Self: Sized,
361 {
362 self.update();
363
364 let vertical =
366 Layout::vertical([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)]);
367 let [header_area, middle_area, input_area] = vertical.areas(area);
368
369 let header_block = Block::default()
370 .borders(Borders::NONE)
371 .fg(TEXT_COLOR)
372 .bg(HEADER_BG)
373 .title("Room view")
374 .title_alignment(Alignment::Center);
375
376 let middle_block = Block::default()
377 .border_set(symbols::border::THICK)
378 .bg(NORMAL_ROW_COLOR)
379 .padding(Padding::horizontal(1));
380
381 header_block.render(header_area, buf);
383 middle_block.render(middle_area, buf);
384
385 let render_paragraph = |buf: &mut Buffer, content: String| {
387 Paragraph::new(content)
388 .fg(TEXT_COLOR)
389 .wrap(Wrap { trim: false })
390 .render(middle_area, buf);
391 };
392
393 if let Some(room_id) = self.selected_room.as_deref() {
394 let rooms = self.ui_rooms.lock();
395 let mut maybe_room = rooms.get(room_id);
396
397 let timeline_area = match &mut self.mode {
398 Mode::Normal { invited_room_view } => {
399 if let Some(view) = invited_room_view {
400 view.render(middle_area, buf);
401
402 None
403 } else {
404 self.input.render(input_area, buf, &mut maybe_room);
405 Some(middle_area)
406 }
407 }
408 Mode::Details { tiling_direction, view } => {
409 let vertical = Layout::new(
410 *tiling_direction,
411 [Constraint::Percentage(50), Constraint::Percentage(50)],
412 );
413 let [timeline_area, details_area] = vertical.areas(middle_area);
414 Clear.render(details_area, buf);
415
416 view.render(details_area, buf, &mut maybe_room);
417
418 Some(timeline_area)
419 }
420 };
421
422 if let Some(items) =
423 self.timelines.lock().get(room_id).map(|timeline| timeline.items.clone())
424 {
425 if let Some(timeline_area) = timeline_area {
426 let items = items.lock();
427 let mut timeline = TimelineView::new(items.deref());
428
429 timeline.render(timeline_area, buf);
430 }
431 }
432 } else {
433 render_paragraph(buf, "Nothing to see here...".to_owned())
434 };
435 }
436}