multiverse/widgets/recovery/
default.rs1use std::{
2 sync::{
3 Arc,
4 atomic::{AtomicBool, Ordering},
5 },
6 time::Duration,
7};
8
9use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
10use futures_util::FutureExt as _;
11use layout::Flex;
12use matrix_sdk::{
13 Client,
14 encryption::{
15 backups::BackupState,
16 recovery::{RecoveryError, RecoveryState},
17 },
18};
19use matrix_sdk_common::executor::spawn;
20use ratatui::{
21 prelude::*,
22 widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
23};
24use throbber_widgets_tui::{Throbber, ThrobberState};
25use tokio::task::JoinHandle;
26
27use super::{ShouldExit, create_centered_throbber_area};
28
29#[derive(Debug)]
30pub struct DefaultRecoveryView {
31 client: Client,
32 recovery_state: RecoveryState,
33 backup_info: BackupInfo,
34 state: ListState,
35 mode: Mode,
36}
37
38#[derive(Debug)]
39struct BackupInfo {
40 backup_state: BackupState,
41 backup_exists: Arc<AtomicBool>,
42 backup_update_task: JoinHandle<()>,
43}
44
45impl Drop for BackupInfo {
46 fn drop(&mut self) {
47 self.backup_update_task.abort();
48 }
49}
50
51#[derive(Debug, Default)]
52enum Mode {
53 #[default]
54 Default,
55 Enabling {
56 enable_task: JoinHandle<Result<String, RecoveryError>>,
57 throbber_state: ThrobberState,
58 },
59 Disabling {
60 disable_task: JoinHandle<Result<(), RecoveryError>>,
61 throbber_state: ThrobberState,
62 },
63 Done {
64 result: DoneResult,
65 },
66}
67
68#[derive(Debug)]
69enum DoneResult {
70 Enabling(Result<String, RecoveryError>),
71 Disabling(Result<(), RecoveryError>),
72}
73
74enum MenuEntries {
75 Recovery = 0,
76 KeyStorage = 1,
77}
78
79impl From<usize> for MenuEntries {
80 fn from(value: usize) -> Self {
81 match value {
82 0 => MenuEntries::Recovery,
83 1 => MenuEntries::KeyStorage,
84 _ => unreachable!("The recovery disabled view has only 2 options"),
85 }
86 }
87}
88
89impl DefaultRecoveryView {
90 pub fn new(client: Client) -> Self {
91 let mut state = ListState::default();
92 state.select_first();
93
94 let recovery_state = client.encryption().recovery().state();
95 let backup_state = client.encryption().backups().state();
96 let backup_exists = Arc::new(AtomicBool::default());
97
98 let backup_update_task = spawn({
99 let client = client.clone();
100 let backup_exists = backup_exists.clone();
101
102 async move {
103 loop {
104 if let Ok(exists) = client.encryption().backups().fetch_exists_on_server().await
105 {
106 backup_exists.store(exists, Ordering::SeqCst);
107 }
108
109 tokio::time::sleep(Duration::from_secs(2)).await;
110 }
111 }
112 });
113
114 let backup_info = BackupInfo { backup_state, backup_exists, backup_update_task };
115
116 Self { client, state, recovery_state, backup_info, mode: Mode::default() }
117 }
118
119 fn handle_recovery_action(&mut self) {
120 let client = self.client.clone();
121
122 if matches!(self.recovery_state, RecoveryState::Disabled) {
123 let enable_task = 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 = 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}