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};
7use tui_framework_experiment::{Button, button};
8
9use crate::widgets::recovery::create_centered_throbber_area;
10
11enum Mode {
12    Loading { task: JoinHandle<Result<Invite, matrix_sdk::Error>> },
13    Joining { task: JoinHandle<Result<(), matrix_sdk::Error>> },
14    Leaving { task: JoinHandle<Result<(), matrix_sdk::Error>> },
15    Loaded { invite_details: Invite },
16    Done,
17}
18
19impl Drop for Mode {
20    fn drop(&mut self) {
21        match self {
22            Mode::Loading { task } => task.abort(),
23            Mode::Joining { task } => task.abort(),
24            Mode::Leaving { task } => task.abort(),
25            Mode::Loaded { .. } => {}
26            Mode::Done => {}
27        }
28    }
29}
30
31enum FocusedButton {
32    Accept = 0,
33    Reject = 1,
34}
35
36impl TryFrom<usize> for FocusedButton {
37    type Error = ();
38
39    fn try_from(value: usize) -> Result<Self, Self::Error> {
40        match value {
41            0 => Ok(FocusedButton::Accept),
42            1 => Ok(FocusedButton::Reject),
43            _ => Err(()),
44        }
45    }
46}
47
48pub struct InvitedRoomView {
49    mode: Mode,
50    room: Room,
51    buttons: Buttons,
52}
53
54struct Buttons {
55    areas: Vec<Rect>,
56    focused_button: FocusedButton,
57    accept: Button<'static>,
58    reject: Button<'static>,
59}
60
61impl Buttons {
62    fn focused_button_mut(&mut self) -> &mut Button<'static> {
63        match self.focused_button {
64            FocusedButton::Accept => &mut self.accept,
65            FocusedButton::Reject => &mut self.reject,
66        }
67    }
68
69    fn focus_next_button(&mut self) {
70        self.focused_button_mut().normal();
71
72        match self.focused_button {
73            FocusedButton::Accept => self.focused_button = FocusedButton::Reject,
74            FocusedButton::Reject => self.focused_button = FocusedButton::Accept,
75        }
76
77        self.focused_button_mut().select();
78    }
79
80    fn release(&mut self) {
81        self.focused_button_mut().select();
82    }
83
84    fn click(&mut self, column: u16, row: u16) -> bool {
85        for (i, area) in self.areas.iter().enumerate() {
86            let area_contains_click = area.left() <= column
87                && column < area.right()
88                && area.top() <= row
89                && row < area.bottom();
90
91            if area_contains_click {
92                match i.try_into() {
93                    Ok(FocusedButton::Accept) => {
94                        self.release();
95                        self.accept.toggle_press();
96                        self.focused_button = FocusedButton::Accept;
97                        return true;
98                    }
99                    Ok(FocusedButton::Reject) => {
100                        self.release();
101                        self.reject.toggle_press();
102                        self.focused_button = FocusedButton::Accept;
103                        return true;
104                    }
105                    _ => {}
106                }
107
108                break;
109            }
110        }
111
112        false
113    }
114}
115
116impl InvitedRoomView {
117    pub(super) fn new(room: Room) -> Self {
118        let task = spawn({
119            let room = room.clone();
120            async move { room.invite_details().await }
121        });
122
123        let mut accept = Button::new("Accept").with_theme(button::themes::GREEN);
124        accept.select();
125
126        let mode = Mode::Loading { task };
127        let buttons = Buttons {
128            focused_button: FocusedButton::Accept,
129            accept,
130            reject: Button::new("Reject").with_theme(button::themes::RED),
131            areas: Vec::new(),
132        };
133
134        Self { mode, room, buttons }
135    }
136
137    fn join_or_leave(&mut self) {
138        let room = self.room.clone();
139
140        let mode = match self.buttons.focused_button {
141            FocusedButton::Accept => {
142                Mode::Joining { task: spawn(async move { room.join().await }) }
143            }
144            FocusedButton::Reject => {
145                Mode::Leaving { task: spawn(async move { room.leave().await }) }
146            }
147        };
148
149        self.mode = mode;
150    }
151
152    pub fn handle_event(&mut self, event: Event) {
153        use KeyCode::*;
154
155        match event {
156            Event::Key(KeyEvent { code: Char('j') | Left, .. }) => self.buttons.focus_next_button(),
157            Event::Key(KeyEvent { code: Char('k') | Right, .. }) => {
158                self.buttons.focus_next_button()
159            }
160            Event::Key(KeyEvent { code: Char(' ') | Enter, .. }) => {
161                self.buttons.focused_button_mut().toggle_press();
162                self.join_or_leave()
163            }
164
165            Event::Mouse(MouseEvent {
166                kind: MouseEventKind::Down(MouseButton::Left),
167                column,
168                row,
169                ..
170            }) => {
171                if self.buttons.click(column, row) {
172                    self.join_or_leave()
173                }
174            }
175
176            Event::Mouse(MouseEvent { kind: MouseEventKind::Up(MouseButton::Left), .. }) => {
177                self.buttons.release();
178            }
179
180            _ => {}
181        }
182    }
183
184    fn update(&mut self) {
185        if !matches!(self.room.state(), RoomState::Invited) {
186            match &mut self.mode {
187                // Don't go into the `Done` mode before the task doing the join finishes. This is
188                // especially important for the shared room history feature, since we do a bunch of
189                // work after the `/join` request is sent out to import the historic room keys.
190                //
191                // This prevents the task from being aborted because switching to the joined room
192                // view is decided by the `should_switch()` 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}