multiverse/widgets/recovery/
default.rs
1use std::{
2 sync::{
3 atomic::{AtomicBool, Ordering},
4 Arc,
5 },
6 time::Duration,
7};
8
9use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
10use futures_util::FutureExt as _;
11use layout::Flex;
12use matrix_sdk::{
13 encryption::{
14 backups::BackupState,
15 recovery::{RecoveryError, RecoveryState},
16 },
17 Client,
18};
19use ratatui::{
20 prelude::*,
21 widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
22};
23use throbber_widgets_tui::{Throbber, ThrobberState};
24use tokio::task::JoinHandle;
25
26use super::{create_centered_throbber_area, ShouldExit};
27
28#[derive(Debug)]
29pub struct DefaultRecoveryView {
30 client: Client,
31 recovery_state: RecoveryState,
32 backup_info: BackupInfo,
33 state: ListState,
34 mode: Mode,
35}
36
37#[derive(Debug)]
38struct BackupInfo {
39 backup_state: BackupState,
40 backup_exists: Arc<AtomicBool>,
41 backup_update_task: JoinHandle<()>,
42}
43
44impl Drop for BackupInfo {
45 fn drop(&mut self) {
46 self.backup_update_task.abort();
47 }
48}
49
50#[derive(Debug, Default)]
51enum Mode {
52 #[default]
53 Default,
54 Enabling {
55 enable_task: JoinHandle<Result<String, RecoveryError>>,
56 throbber_state: ThrobberState,
57 },
58 Disabling {
59 disable_task: JoinHandle<Result<(), RecoveryError>>,
60 throbber_state: ThrobberState,
61 },
62 Done {
63 result: DoneResult,
64 },
65}
66
67#[derive(Debug)]
68enum DoneResult {
69 Enabling(Result<String, RecoveryError>),
70 Disabling(Result<(), RecoveryError>),
71}
72
73enum MenuEntries {
74 Recovery = 0,
75 KeyStorage = 1,
76}
77
78impl From<usize> for MenuEntries {
79 fn from(value: usize) -> Self {
80 match value {
81 0 => MenuEntries::Recovery,
82 1 => MenuEntries::KeyStorage,
83 _ => unreachable!("The recovery disabled view has only 2 options"),
84 }
85 }
86}
87
88impl DefaultRecoveryView {
89 pub fn new(client: Client) -> Self {
90 let mut state = ListState::default();
91 state.select_first();
92
93 let recovery_state = client.encryption().recovery().state();
94 let backup_state = client.encryption().backups().state();
95 let backup_exists = Arc::new(AtomicBool::default());
96
97 let backup_update_task = tokio::spawn({
98 let client = client.clone();
99 let backup_exists = backup_exists.clone();
100
101 async move {
102 loop {
103 if let Ok(exists) = client.encryption().backups().fetch_exists_on_server().await
104 {
105 backup_exists.store(exists, Ordering::SeqCst);
106 }
107
108 tokio::time::sleep(Duration::from_secs(2)).await;
109 }
110 }
111 });
112
113 let backup_info = BackupInfo { backup_state, backup_exists, backup_update_task };
114
115 Self { client, state, recovery_state, backup_info, mode: Mode::default() }
116 }
117
118 fn handle_recovery_action(&mut self) {
119 let client = self.client.clone();
120
121 if matches!(self.recovery_state, RecoveryState::Disabled) {
122 let enable_task =
123 tokio::spawn(async move { client.encryption().recovery().enable().await });
124
125 self.mode = Mode::Enabling { enable_task, throbber_state: ThrobberState::default() };
126 } else {
127 let disable_task = tokio::spawn(async move {
128 let _ = client.encryption().recovery().disable().await;
130 Ok(())
131 });
132
133 self.mode = Mode::Disabling { disable_task, throbber_state: ThrobberState::default() };
134 }
135 }
136
137 async fn handle_backup_action(&mut self) {
138 let backup_state = self.backup_info.backup_state;
139 let backup_exists = self.backup_info.backup_exists.load(Ordering::SeqCst);
140
141 match (backup_state, backup_exists) {
142 (BackupState::Unknown, false) => {
143 let _ = self.client.encryption().backups().create().await;
144 }
145 (BackupState::Unknown, true) | (BackupState::Enabled, _) => {
146 let _ = self.client.encryption().backups().disable_and_delete().await;
147 self.backup_info.backup_exists.store(false, Ordering::SeqCst);
148 }
149 (BackupState::Creating, _)
150 | (BackupState::Enabling, _)
151 | (BackupState::Resuming, _)
152 | (BackupState::Downloading, _)
153 | (BackupState::Disabling, _) => {}
154 }
155 }
156
157 pub async fn handle_key(&mut self, key: KeyEvent) -> ShouldExit {
158 use ShouldExit::*;
159
160 if key.kind != KeyEventKind::Press {
161 return No;
162 }
163
164 match self.mode {
165 Mode::Default => match key.code {
166 KeyCode::Esc | KeyCode::Char('q') => Yes,
167 KeyCode::Char('j') | KeyCode::Down => {
168 self.state.select_next();
169 No
170 }
171 KeyCode::Char('k') | KeyCode::Up => {
172 self.state.select_previous();
173 No
174 }
175 KeyCode::Enter | KeyCode::Char(' ') => {
176 if let Some(selected) = self.state.selected() {
177 match selected.into() {
178 MenuEntries::Recovery => self.handle_recovery_action(),
179 MenuEntries::KeyStorage => self.handle_backup_action().await,
180 }
181 }
182
183 No
184 }
185 _ => No,
186 },
187 Mode::Enabling { .. } | Mode::Disabling { .. } => No,
188 Mode::Done { .. } => {
189 self.mode = Mode::Default;
190 OnlySubScreen
191 }
192 }
193 }
194
195 pub fn on_tick(&mut self) {
196 use Mode::*;
197
198 match &mut self.mode {
199 Enabling { throbber_state, .. } | Disabling { throbber_state, .. } => {
200 throbber_state.calc_next()
201 }
202 Default | Done { .. } => {}
203 }
204 }
205
206 pub fn is_idle(&self) -> bool {
207 match self.mode {
208 Mode::Default => true,
209 Mode::Enabling { .. } | Mode::Disabling { .. } | Mode::Done { .. } => false,
210 }
211 }
212
213 fn update_state(&mut self) {
214 use Mode::*;
215
216 let recovery_state = self.client.encryption().recovery().state();
217 let backup_state = self.client.encryption().backups().state();
218
219 self.recovery_state = recovery_state;
220 self.backup_info.backup_state = backup_state;
221
222 match &mut self.mode {
223 Default => {}
224 Enabling { enable_task, .. } => {
226 if enable_task.is_finished() {
227 let result = enable_task
228 .now_or_never()
229 .expect("The task should have finished, we checked it")
230 .expect("The recovery enabling task should neve panic");
231 self.mode = Done { result: DoneResult::Enabling(result) };
232 }
233 }
234 Disabling { disable_task, .. } => {
235 if disable_task.is_finished() {
236 let result = disable_task
237 .now_or_never()
238 .expect("The task should have finished, we checked it")
239 .expect("The recovery enabling task should neve panic");
240 self.mode = Done { result: DoneResult::Disabling(result) };
241 }
242 }
243
244 Done { .. } => {}
246 }
247 }
248}
249
250impl Widget for &mut DefaultRecoveryView {
251 fn render(self, area: Rect, buf: &mut Buffer)
252 where
253 Self: Sized,
254 {
255 self.update_state();
256
257 let style = match &self.mode {
258 Mode::Default => Style::default(),
259 Mode::Enabling { .. } | Mode::Done { .. } | Mode::Disabling { .. } => {
260 Style::default().dim()
261 }
262 };
263
264 let recovery_item = match self.recovery_state {
265 RecoveryState::Unknown => {
266 ListItem::new("Recovery [?]").style(Style::default().dim())
267 }
268 RecoveryState::Enabled => ListItem::new("Recovery [x]").style(style),
269 RecoveryState::Disabled | RecoveryState::Incomplete => {
270 ListItem::new("Recovery [ ]").style(style)
271 }
272 };
273
274 let backup_state = self.backup_info.backup_state;
275 let backup_exists = self.backup_info.backup_exists.load(Ordering::SeqCst);
276
277 let backups = match (backup_state, backup_exists) {
278 (BackupState::Unknown, true) => {
279 ListItem::new("Key storage [~] (a backup exists but we don't have access to it)")
280 .dim()
281 }
282 (BackupState::Unknown, false) => ListItem::new("Key storage [ ]"),
283 (BackupState::Creating, _)
284 | (BackupState::Enabling, _)
285 | (BackupState::Resuming, _) => ListItem::new("Key storage [x]").dim(),
286 (BackupState::Enabled, true) => ListItem::new("Key storage [x]"),
287 (BackupState::Enabled, false) => ListItem::new("Key storage [x]"),
288 (BackupState::Downloading, _) | (BackupState::Disabling, _) => {
289 ListItem::new("Key storage [ ]").dim()
290 }
291 };
292
293 let list = List::new(vec![recovery_item, backups])
294 .highlight_symbol("> ")
295 .highlight_spacing(ratatui::widgets::HighlightSpacing::Always);
296
297 StatefulWidget::render(list, area, buf, &mut self.state);
298
299 match &mut self.mode {
300 Mode::Default => {}
301 Mode::Enabling { throbber_state, .. } => {
302 let throbber = Throbber::default()
303 .label("Enabling recovery")
304 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
305 let centered_area = create_centered_throbber_area(area);
306 StatefulWidget::render(throbber, centered_area, buf, throbber_state);
307 }
308 Mode::Disabling { throbber_state, .. } => {
309 let throbber = Throbber::default()
310 .label("Disabling recovery")
311 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
312 let centered_area = create_centered_throbber_area(area);
313 StatefulWidget::render(throbber, centered_area, buf, throbber_state);
314 }
315
316 Mode::Done { result } => {
317 let vertical = Layout::vertical([
318 Constraint::Fill(1),
319 Constraint::Length(4),
320 Constraint::Fill(1),
321 ])
322 .flex(Flex::Center);
323 let horizontal = Layout::horizontal([Constraint::Length(70)]).flex(Flex::Center);
324 let [_, area, _] = vertical.areas(area);
325 let [popup] = horizontal.areas(area);
326
327 Clear.render(popup, buf);
328
329 let block = Block::new().borders(Borders::all());
330
331 let text = match result {
332 DoneResult::Enabling(Ok(recovery_key)) => {
333 format!("Recovery has been enabled:\n{recovery_key}")
334 }
335 DoneResult::Enabling(Err(error)) => {
336 format!("Failed to enable recovery: {error:?}")
337 }
338 DoneResult::Disabling(Ok(())) => "Recovery has been disabled".to_owned(),
339 DoneResult::Disabling(Err(error)) => {
340 format!("Failed to disable recovery: {error:?}")
341 }
342 };
343
344 Paragraph::new(text).centered().block(block).render(popup, buf);
345 }
346 }
347 }
348}