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 finishes. This is
186                // especially important for the shared room history feature, since we do a bunch of
187                // work after the `/join` request is sent out to import the historic room keys.
188                //
189                // This prevents the task from being aborted because switching to the joined room
190                // view is decided by the `should_switch()` function.
191                Mode::Joining { task } => {
192                    if task.is_finished() {
193                        self.mode = Mode::Done
194                    }
195                }
196                Mode::Loading { .. } | Mode::Leaving { .. } | Mode::Loaded { .. } | Mode::Done => {
197                    self.mode = Mode::Done
198                }
199            }
200        } else {
201            match &mut self.mode {
202                Mode::Loading { task } => {
203                    if task.is_finished() {
204                        let invite_details = task
205                            .now_or_never()
206                            .expect("We checked that the task has finished")
207                            .expect("The task shouldn't ever panic")
208                            .expect("We should be able to load the invite details from storage");
209                        self.mode = Mode::Loaded { invite_details };
210                    }
211                }
212                Mode::Joining { .. } => {}
213                Mode::Leaving { .. } => {}
214                Mode::Loaded { .. } => {}
215                Mode::Done => {}
216            }
217        }
218    }
219
220    pub fn should_switch(&self) -> bool {
221        matches!(self.mode, Mode::Done)
222    }
223}
224
225impl Widget for &mut InvitedRoomView {
226    fn render(self, area: Rect, buf: &mut Buffer)
227    where
228        Self: Sized,
229    {
230        self.update();
231
232        let mut create_throbber = |title| {
233            let centered = create_centered_throbber_area(area);
234            let mut state = ThrobberState::default();
235            state.calc_step(0);
236
237            let throbber = Throbber::default()
238                .label(title)
239                .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
240
241            StatefulWidget::render(throbber, centered, buf, &mut state);
242        };
243
244        match &self.mode {
245            Mode::Loading { .. } => create_throbber("Loading"),
246            Mode::Leaving { .. } => create_throbber("Rejecting"),
247            Mode::Joining { .. } | Mode::Done => create_throbber("Joining"),
248            Mode::Loaded { invite_details } => {
249                let text = if let Some(inviter) = &invite_details.inviter {
250                    let display_name =
251                        inviter.display_name().unwrap_or_else(|| inviter.user_id().as_str());
252
253                    format!("{display_name} has invited you to this room")
254                } else {
255                    "You have been invited to this room".to_owned()
256                };
257
258                let [_, middle_area, _] = Layout::default()
259                    .direction(Direction::Vertical)
260                    .margin(1)
261                    .constraints([Constraint::Fill(1), Constraint::Length(5), Constraint::Fill(1)])
262                    .areas(area);
263
264                let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(5)]);
265                let [label_area, button_area] = vertical.areas(middle_area);
266
267                let paragraph = Paragraph::new(text).centered();
268                paragraph.render(label_area, buf);
269
270                let [_, left_button, _, right_button, _] = Layout::horizontal([
271                    Constraint::Fill(1),
272                    Constraint::Length(20),
273                    Constraint::Length(1),
274                    Constraint::Length(20),
275                    Constraint::Fill(1),
276                ])
277                .areas(button_area);
278
279                self.buttons.areas = vec![left_button, right_button];
280
281                self.buttons.accept.render(left_button, buf);
282                self.buttons.reject.render(right_button, buf);
283            }
284        }
285    }
286}