multiverse/widgets/recovery/
mod.rs

1use crossterm::event::{KeyCode, KeyEvent};
2use matrix_sdk::{encryption::recovery::RecoveryState, Client};
3use ratatui::prelude::*;
4use recovering::RecoveringView;
5use throbber_widgets_tui::{Throbber, ThrobberState};
6
7mod default;
8mod recovering;
9
10use default::DefaultRecoveryView;
11
12#[derive(Default)]
13pub struct RecoveryView {}
14
15impl RecoveryView {
16    pub fn new() -> Self {
17        Self {}
18    }
19}
20
21impl RecoveryView {}
22
23pub struct RecoveryViewState {
24    client: Client,
25    throbber_state: ThrobberState,
26    mode: Mode,
27}
28
29#[derive(Debug, Default)]
30enum Mode {
31    #[default]
32    Unknown,
33    Incomplete {
34        view: RecoveringView,
35    },
36    Default {
37        view: DefaultRecoveryView,
38    },
39}
40
41pub enum ShouldExit {
42    No,
43    OnlySubScreen,
44    Yes,
45}
46
47impl RecoveryViewState {
48    pub fn new(client: Client) -> Self {
49        Self { client, throbber_state: ThrobberState::default(), mode: Mode::default() }
50    }
51
52    fn update_state(&mut self) {
53        let recovery_state = self.client.encryption().recovery().state();
54
55        match (&mut self.mode, recovery_state) {
56            // We were in the unknown mode, showing a throbber, but now we figured out that
57            // recovery either exists and there's nothing much to do, or we can enable it.
58            //
59            // Let's switch to our default view which allows recovery to be disabled or enabled.
60            (Mode::Unknown, RecoveryState::Disabled | RecoveryState::Enabled) => {
61                self.mode = Mode::Default { view: DefaultRecoveryView::new(self.client.clone()) };
62            }
63
64            // The recovery state changed to incomplete, we go into the incomplete view so users
65            // can input the recovery key or reset recovery.
66            (Mode::Unknown, RecoveryState::Incomplete) => {
67                let view = RecoveringView::new(self.client.clone());
68                self.mode = Mode::Incomplete { view }
69            }
70
71            // We were showing the incomplete view but someone disabled recovery on another device,
72            // let's change the screen to reflect that.
73            (Mode::Incomplete { view }, RecoveryState::Disabled) => {
74                if view.is_idle() {
75                    self.mode =
76                        Mode::Default { view: DefaultRecoveryView::new(self.client.clone()) }
77                }
78            }
79
80            (Mode::Incomplete { view }, RecoveryState::Enabled) => {
81                if view.is_idle() {
82                    self.mode =
83                        Mode::Default { view: DefaultRecoveryView::new(self.client.clone()) }
84                }
85            }
86
87            (Mode::Default { view }, RecoveryState::Incomplete) => {
88                if view.is_idle() {
89                    let view = RecoveringView::new(self.client.clone());
90                    self.mode = Mode::Incomplete { view }
91                }
92            }
93
94            // The recovery state didn't change in comparison to our desired view.
95            (Mode::Incomplete { .. }, RecoveryState::Incomplete)
96            | (Mode::Default { .. }, RecoveryState::Disabled | RecoveryState::Enabled)
97            | (Mode::Unknown, RecoveryState::Unknown) => {}
98
99            // The recovery state changed back to `Unknown`? This can never
100            // happen but let's just go back to the `Unknown` view
101            // showing a throbber.
102            (Mode::Default { .. }, RecoveryState::Unknown)
103            | (Mode::Incomplete { .. }, RecoveryState::Unknown) => {
104                self.mode = Mode::Unknown;
105            }
106        }
107    }
108
109    pub async fn handle_key_press(&mut self, key: KeyEvent) -> bool {
110        use KeyCode::*;
111
112        match &mut self.mode {
113            Mode::Unknown => matches!((key.modifiers, key.code), (_, Esc | Char('q'))),
114            Mode::Incomplete { view } => match view.handle_key(key) {
115                ShouldExit::No => false,
116                ShouldExit::OnlySubScreen => {
117                    self.mode = Mode::Unknown;
118                    false
119                }
120                ShouldExit::Yes => true,
121            },
122            Mode::Default { view } => match view.handle_key(key).await {
123                ShouldExit::No => false,
124                ShouldExit::OnlySubScreen => {
125                    self.mode = Mode::Unknown;
126                    false
127                }
128                ShouldExit::Yes => true,
129            },
130        }
131    }
132
133    pub fn on_tick(&mut self) {
134        self.throbber_state.calc_next();
135
136        match &mut self.mode {
137            Mode::Unknown => (),
138            Mode::Incomplete { view } => view.on_tick(),
139            Mode::Default { view } => view.on_tick(),
140        }
141    }
142
143    fn get_throbber(&self, title: &'static str) -> Throbber<'static> {
144        Throbber::default().label(title).throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE)
145    }
146}
147
148pub fn create_centered_throbber_area(area: Rect) -> Rect {
149    let chunks = Layout::default()
150        .direction(Direction::Horizontal)
151        .margin(1)
152        .constraints([Constraint::Fill(1), Constraint::Length(12), Constraint::Fill(1)])
153        .split(area);
154
155    let chunks = Layout::default()
156        .direction(Direction::Vertical)
157        .margin(1)
158        .constraints([Constraint::Fill(1), Constraint::Length(1), Constraint::Fill(1)])
159        .split(chunks[1]);
160
161    chunks[1]
162}
163
164impl StatefulWidget for &mut RecoveryView {
165    type State = RecoveryViewState;
166
167    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
168        state.update_state();
169
170        // Let's now render our current screen.
171        match &mut state.mode {
172            Mode::Unknown => {
173                let throbber = state.get_throbber("Loading");
174                let centered_area = create_centered_throbber_area(area);
175                StatefulWidget::render(throbber, centered_area, buf, &mut state.throbber_state);
176            }
177            Mode::Default { view } => {
178                view.render(area, buf);
179            }
180            Mode::Incomplete { view } => {
181                view.render(area, buf);
182            }
183        }
184    }
185}