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    /// Room list service rooms known to the app.
40    ui_rooms: UiRooms,
41
42    /// Timelines data structures for each room.
43    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    /// Run a small back-pagination (expect a batch of 20 events, continue until
204    /// we get 10 timeline items or hit the timeline start).
205    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        // Cancel the previous back-pagination, if any.
216        if let Some(prev) = pagination.take() {
217            prev.abort();
218        }
219
220        let status_handle = self.status_handle.clone();
221
222        // Start a new one, request batches of 20 events, stop after 10 timeline items
223        // have been added.
224        *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            // Look for the latest (most recent) room message.
241            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 found, send a reaction.
250            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    /// Mark the currently selected room as read.
321    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        // Mark as read!
331        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        // Create a space for the header, timeline, and input area.
365        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        // Let's render the backgrounds for the header and the timeline.
382        header_block.render(header_area, buf);
383        middle_block.render(middle_area, buf);
384
385        // Helper to render some string as a paragraph.
386        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}