multiverse/widgets/room_view/
invited_room.rs1use 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 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}