Skip to main content

multiverse/widgets/room_view/
invited_room.rs

1use crossterm::event::{Event, KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
2use futures_util::FutureExt;
3use matrix_sdk::{Room, RoomState, room::Invite};
4use ratatui::{prelude::*, widgets::*};
5use throbber_widgets_tui::{Throbber, ThrobberState};
6use tokio::{spawn, task::JoinHandle};
7
8use crate::widgets::{
9    button::{self, Button},
10    recovery::create_centered_throbber_area,
11};
12
13enum Mode {
14    Loading { task: JoinHandle<Result<Invite, matrix_sdk::Error>> },
15    Joining { task: JoinHandle<Result<(), matrix_sdk::Error>> },
16    Leaving { task: JoinHandle<Result<(), matrix_sdk::Error>> },
17    Loaded { invite_details: Invite },
18    Done,
19}
20
21impl Drop for Mode {
22    fn drop(&mut self) {
23        match self {
24            Mode::Loading { task } => task.abort(),
25            Mode::Joining { task } => task.abort(),
26            Mode::Leaving { task } => task.abort(),
27            Mode::Loaded { .. } => {}
28            Mode::Done => {}
29        }
30    }
31}
32
33enum FocusedButton {
34    Accept = 0,
35    Reject = 1,
36}
37
38impl TryFrom<usize> for FocusedButton {
39    type Error = ();
40
41    fn try_from(value: usize) -> Result<Self, Self::Error> {
42        match value {
43            0 => Ok(FocusedButton::Accept),
44            1 => Ok(FocusedButton::Reject),
45            _ => Err(()),
46        }
47    }
48}
49
50pub struct InvitedRoomView {
51    mode: Mode,
52    room: Room,
53    buttons: Buttons,
54}
55
56struct Buttons {
57    areas: Vec<Rect>,
58    focused_button: FocusedButton,
59    accept: Button<'static>,
60    reject: Button<'static>,
61}
62
63impl Buttons {
64    fn focused_button_mut(&mut self) -> &mut Button<'static> {
65        match self.focused_button {
66            FocusedButton::Accept => &mut self.accept,
67            FocusedButton::Reject => &mut self.reject,
68        }
69    }
70
71    fn focus_next_button(&mut self) {
72        self.focused_button_mut().normal();
73
74        match self.focused_button {
75            FocusedButton::Accept => self.focused_button = FocusedButton::Reject,
76            FocusedButton::Reject => self.focused_button = FocusedButton::Accept,
77        }
78
79        self.focused_button_mut().select();
80    }
81
82    fn release(&mut self) {
83        self.focused_button_mut().select();
84    }
85
86    fn click(&mut self, column: u16, row: u16) -> bool {
87        for (i, area) in self.areas.iter().enumerate() {
88            let area_contains_click = area.left() <= column
89                && column < area.right()
90                && area.top() <= row
91                && row < area.bottom();
92
93            if area_contains_click {
94                match i.try_into() {
95                    Ok(FocusedButton::Accept) => {
96                        self.release();
97                        self.accept.toggle_press();
98                        self.focused_button = FocusedButton::Accept;
99                        return true;
100                    }
101                    Ok(FocusedButton::Reject) => {
102                        self.release();
103                        self.reject.toggle_press();
104                        self.focused_button = FocusedButton::Accept;
105                        return true;
106                    }
107                    _ => {}
108                }
109
110                break;
111            }
112        }
113
114        false
115    }
116}
117
118impl InvitedRoomView {
119    pub(super) fn new(room: Room) -> Self {
120        let task = spawn({
121            let room = room.clone();
122            async move { room.invite_details().await }
123        });
124
125        let mut accept = Button::new("Accept").with_theme(button::themes::GREEN);
126        accept.select();
127
128        let mode = Mode::Loading { task };
129        let buttons = Buttons {
130            focused_button: FocusedButton::Accept,
131            accept,
132            reject: Button::new("Reject").with_theme(button::themes::RED),
133            areas: Vec::new(),
134        };
135
136        Self { mode, room, buttons }
137    }
138
139    fn join_or_leave(&mut self) {
140        let room = self.room.clone();
141
142        let mode = match self.buttons.focused_button {
143            FocusedButton::Accept => {
144                Mode::Joining { task: spawn(async move { room.join().await }) }
145            }
146            FocusedButton::Reject => {
147                Mode::Leaving { task: spawn(async move { room.leave().await }) }
148            }
149        };
150
151        self.mode = mode;
152    }
153
154    pub fn handle_event(&mut self, event: Event) {
155        use KeyCode::*;
156
157        match event {
158            Event::Key(KeyEvent { code: Char('j') | Left, .. }) => self.buttons.focus_next_button(),
159            Event::Key(KeyEvent { code: Char('k') | Right, .. }) => {
160                self.buttons.focus_next_button()
161            }
162            Event::Key(KeyEvent { code: Char(' ') | Enter, .. }) => {
163                self.buttons.focused_button_mut().toggle_press();
164                self.join_or_leave()
165            }
166
167            Event::Mouse(MouseEvent {
168                kind: MouseEventKind::Down(MouseButton::Left),
169                column,
170                row,
171                ..
172            }) if self.buttons.click(column, row) => self.join_or_leave(),
173
174            Event::Mouse(MouseEvent { kind: MouseEventKind::Up(MouseButton::Left), .. }) => {
175                self.buttons.release();
176            }
177
178            _ => {}
179        }
180    }
181
182    fn update(&mut self) {
183        if !matches!(self.room.state(), RoomState::Invited) {
184            match &mut self.mode {
185                // Don't go into the `Done` mode before the task doing the join
186                // finishes. This is especially important for the shared room
187                // history feature, since we do a bunch of work after the
188                // `/join` request is sent out to import the historic room keys.
189                //
190                // This prevents the task from being aborted because switching
191                // to the joined room view is decided by the `should_switch()`
192                // function.
193                Mode::Joining { task } => {
194                    if task.is_finished() {
195                        self.mode = Mode::Done
196                    }
197                }
198                Mode::Loading { .. } | Mode::Leaving { .. } | Mode::Loaded { .. } | Mode::Done => {
199                    self.mode = Mode::Done
200                }
201            }
202        } else {
203            match &mut self.mode {
204                Mode::Loading { task } => {
205                    if task.is_finished() {
206                        let invite_details = task
207                            .now_or_never()
208                            .expect("We checked that the task has finished")
209                            .expect("The task shouldn't ever panic")
210                            .expect("We should be able to load the invite details from storage");
211                        self.mode = Mode::Loaded { invite_details };
212                    }
213                }
214                Mode::Joining { .. } => {}
215                Mode::Leaving { .. } => {}
216                Mode::Loaded { .. } => {}
217                Mode::Done => {}
218            }
219        }
220    }
221
222    pub fn should_switch(&self) -> bool {
223        matches!(self.mode, Mode::Done)
224    }
225}
226
227impl Widget for &mut InvitedRoomView {
228    fn render(self, area: Rect, buf: &mut Buffer)
229    where
230        Self: Sized,
231    {
232        self.update();
233
234        let mut create_throbber = |title| {
235            let centered = create_centered_throbber_area(area);
236            let mut state = ThrobberState::default();
237            state.calc_step(0);
238
239            let throbber = Throbber::default()
240                .label(title)
241                .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
242
243            StatefulWidget::render(throbber, centered, buf, &mut state);
244        };
245
246        match &self.mode {
247            Mode::Loading { .. } => create_throbber("Loading"),
248            Mode::Leaving { .. } => create_throbber("Rejecting"),
249            Mode::Joining { .. } | Mode::Done => create_throbber("Joining"),
250            Mode::Loaded { invite_details } => {
251                let text = if let Some(inviter) = &invite_details.inviter {
252                    let display_name =
253                        inviter.display_name().unwrap_or_else(|| inviter.user_id().as_str());
254
255                    format!("{display_name} has invited you to this room")
256                } else {
257                    "You have been invited to this room".to_owned()
258                };
259
260                let [_, middle_area, _] = Layout::default()
261                    .direction(Direction::Vertical)
262                    .margin(1)
263                    .constraints([Constraint::Fill(1), Constraint::Length(5), Constraint::Fill(1)])
264                    .areas(area);
265
266                let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(5)]);
267                let [label_area, button_area] = vertical.areas(middle_area);
268
269                let paragraph = Paragraph::new(text).centered();
270                paragraph.render(label_area, buf);
271
272                let [_, left_button, _, right_button, _] = Layout::horizontal([
273                    Constraint::Fill(1),
274                    Constraint::Length(20),
275                    Constraint::Length(1),
276                    Constraint::Length(20),
277                    Constraint::Fill(1),
278                ])
279                .areas(button_area);
280
281                self.buttons.areas = vec![left_button, right_button];
282
283                self.buttons.accept.render(left_button, buf);
284                self.buttons.reject.render(right_button, buf);
285            }
286        }
287    }
288}