Skip to main content

multiverse/widgets/recovery/
mod.rs

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