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};
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 }) if self.buttons.click(column, row) => self.join_or_leave(),
171
172 Event::Mouse(MouseEvent { kind: MouseEventKind::Up(MouseButton::Left), .. }) => {
173 self.buttons.release();
174 }
175
176 _ => {}
177 }
178 }
179
180 fn update(&mut self) {
181 if !matches!(self.room.state(), RoomState::Invited) {
182 match &mut self.mode {
183 Mode::Joining { task } => {
190 if task.is_finished() {
191 self.mode = Mode::Done
192 }
193 }
194 Mode::Loading { .. } | Mode::Leaving { .. } | Mode::Loaded { .. } | Mode::Done => {
195 self.mode = Mode::Done
196 }
197 }
198 } else {
199 match &mut self.mode {
200 Mode::Loading { task } => {
201 if task.is_finished() {
202 let invite_details = task
203 .now_or_never()
204 .expect("We checked that the task has finished")
205 .expect("The task shouldn't ever panic")
206 .expect("We should be able to load the invite details from storage");
207 self.mode = Mode::Loaded { invite_details };
208 }
209 }
210 Mode::Joining { .. } => {}
211 Mode::Leaving { .. } => {}
212 Mode::Loaded { .. } => {}
213 Mode::Done => {}
214 }
215 }
216 }
217
218 pub fn should_switch(&self) -> bool {
219 matches!(self.mode, Mode::Done)
220 }
221}
222
223impl Widget for &mut InvitedRoomView {
224 fn render(self, area: Rect, buf: &mut Buffer)
225 where
226 Self: Sized,
227 {
228 self.update();
229
230 let mut create_throbber = |title| {
231 let centered = create_centered_throbber_area(area);
232 let mut state = ThrobberState::default();
233 state.calc_step(0);
234
235 let throbber = Throbber::default()
236 .label(title)
237 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
238
239 StatefulWidget::render(throbber, centered, buf, &mut state);
240 };
241
242 match &self.mode {
243 Mode::Loading { .. } => create_throbber("Loading"),
244 Mode::Leaving { .. } => create_throbber("Rejecting"),
245 Mode::Joining { .. } | Mode::Done => create_throbber("Joining"),
246 Mode::Loaded { invite_details } => {
247 let text = if let Some(inviter) = &invite_details.inviter {
248 let display_name =
249 inviter.display_name().unwrap_or_else(|| inviter.user_id().as_str());
250
251 format!("{display_name} has invited you to this room")
252 } else {
253 "You have been invited to this room".to_owned()
254 };
255
256 let [_, middle_area, _] = Layout::default()
257 .direction(Direction::Vertical)
258 .margin(1)
259 .constraints([Constraint::Fill(1), Constraint::Length(5), Constraint::Fill(1)])
260 .areas(area);
261
262 let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(5)]);
263 let [label_area, button_area] = vertical.areas(middle_area);
264
265 let paragraph = Paragraph::new(text).centered();
266 paragraph.render(label_area, buf);
267
268 let [_, left_button, _, right_button, _] = Layout::horizontal([
269 Constraint::Fill(1),
270 Constraint::Length(20),
271 Constraint::Length(1),
272 Constraint::Length(20),
273 Constraint::Fill(1),
274 ])
275 .areas(button_area);
276
277 self.buttons.areas = vec![left_button, right_button];
278
279 self.buttons.accept.render(left_button, buf);
280 self.buttons.reject.render(right_button, buf);
281 }
282 }
283 }
284}