feat: split settings into Identity, Server, Backup and Advanced tabs
This commit is contained in:
parent
2892ea3fa7
commit
743c388cfb
4 changed files with 365 additions and 288 deletions
119
src/app.rs
119
src/app.rs
|
|
@ -159,10 +159,6 @@ pub enum Message {
|
|||
SignupRegister,
|
||||
SignupRecall,
|
||||
|
||||
// Pin offer (signup and settings share it)
|
||||
PinAccept,
|
||||
PinDiscard,
|
||||
|
||||
// Mail
|
||||
GotoMail(Folder),
|
||||
Goto(Page),
|
||||
|
|
@ -202,19 +198,17 @@ pub enum Message {
|
|||
ContactAccept(String, String),
|
||||
ContactBlock(String),
|
||||
ContactUnblock(String),
|
||||
CopyExport,
|
||||
|
||||
// Settings
|
||||
SettingsLearnKey,
|
||||
SecretRevealToggled,
|
||||
CopySecret,
|
||||
KeepMailChanged(bool),
|
||||
TimeoutChanged(String),
|
||||
RotatePressed,
|
||||
RotateConfirmed,
|
||||
RotateCancelled,
|
||||
UnpinPressed(String),
|
||||
ExportPressed,
|
||||
ImportBodyChanged(String),
|
||||
ImportPressed,
|
||||
ExportBackupTo(PathBuf),
|
||||
ImportBackupFrom(String),
|
||||
ResetPressed,
|
||||
ResetConfirmed,
|
||||
ResetCancelled,
|
||||
|
|
@ -229,8 +223,6 @@ pub enum OpResult {
|
|||
/// The key a server presented with no pin in place (sec 4 tofu).
|
||||
PinOffered(Result<Option<(Address, [u8; KEY_LEN])>, String>),
|
||||
Pinned(Result<(), String>),
|
||||
/// (note, backup text) — the text is for the clipboard and the view.
|
||||
Exported(Result<(String, String), String>),
|
||||
}
|
||||
|
||||
/// One background operation to run on the blocking pool, carrying every
|
||||
|
|
@ -280,11 +272,8 @@ pub enum PendingOp {
|
|||
uri: String,
|
||||
},
|
||||
Rotate,
|
||||
Unpin {
|
||||
label: String,
|
||||
},
|
||||
Export {
|
||||
dir: PathBuf,
|
||||
path: PathBuf,
|
||||
},
|
||||
ImportBackup {
|
||||
text: String,
|
||||
|
|
@ -322,8 +311,8 @@ pub struct Carrier {
|
|||
pub drafts: Vec<Draft>,
|
||||
pub snapshot: Option<Snapshot>,
|
||||
pub pin_offer: Option<(Address, [u8; KEY_LEN])>,
|
||||
pub backup_text: Option<String>,
|
||||
pub import_body: String,
|
||||
/// The Identity tab's recovery secret is masked until this is toggled.
|
||||
pub secret_revealed: bool,
|
||||
pub contact_import: String,
|
||||
pub dialog: Option<Dialog>,
|
||||
/// The view syncs entry text from the model once after a prefill
|
||||
|
|
@ -389,8 +378,7 @@ impl Carrier {
|
|||
drafts,
|
||||
snapshot: None,
|
||||
pin_offer: None,
|
||||
backup_text: None,
|
||||
import_body: String::new(),
|
||||
secret_revealed: false,
|
||||
contact_import: String::new(),
|
||||
dialog: None,
|
||||
compose_fresh: Cell::new(false),
|
||||
|
|
@ -480,7 +468,7 @@ impl Carrier {
|
|||
}
|
||||
let (app, _) = Carrier::boot(self.dir.clone());
|
||||
*self = app;
|
||||
self.note("factory reset done. Create a new identity or restore a seed.".into());
|
||||
self.note("logged out. Create a new identity or restore a seed.".into());
|
||||
}
|
||||
|
||||
/// Cancelling out of a not-yet-registered identity removes the key
|
||||
|
|
@ -577,11 +565,6 @@ impl Carrier {
|
|||
})
|
||||
}
|
||||
|
||||
/// The account address, for the settings page's learn-key action.
|
||||
fn account_address(&self) -> Option<Address> {
|
||||
self.store.account().ok().flatten()
|
||||
}
|
||||
|
||||
/// `user@host` as typed on the signup page, defaulting the port.
|
||||
fn signup_address(&self) -> Option<Address> {
|
||||
let user = self.signup.user.trim();
|
||||
|
|
@ -656,9 +639,6 @@ impl Carrier {
|
|||
}
|
||||
}
|
||||
}
|
||||
Message::ImportBodyChanged(text) => {
|
||||
self.import_body = text;
|
||||
}
|
||||
Message::ContactSelected(address) => {
|
||||
self.stash_draft();
|
||||
self.selected_own = false;
|
||||
|
|
@ -709,6 +689,9 @@ impl Carrier {
|
|||
Message::KeepMailChanged(keep) => {
|
||||
self.keep_mail = keep;
|
||||
}
|
||||
Message::SecretRevealToggled => {
|
||||
self.secret_revealed = !self.secret_revealed;
|
||||
}
|
||||
Message::TimeoutChanged(text) => {
|
||||
if let Ok(timeout) = text.trim().parse::<u64>() {
|
||||
self.timeout = timeout;
|
||||
|
|
@ -790,21 +773,6 @@ impl Carrier {
|
|||
};
|
||||
return vec![PendingOp::LearnKey { addr }];
|
||||
}
|
||||
Message::SettingsLearnKey => {
|
||||
let Some(addr) = self.account_address() else {
|
||||
return Vec::new();
|
||||
};
|
||||
return vec![PendingOp::LearnKey { addr }];
|
||||
}
|
||||
Message::PinDiscard => {
|
||||
self.pin_offer = None;
|
||||
}
|
||||
Message::PinAccept => {
|
||||
let Some((addr, key)) = self.pin_offer.take() else {
|
||||
return Vec::new();
|
||||
};
|
||||
return vec![PendingOp::Pin { addr, key }];
|
||||
}
|
||||
Message::SignupRegister => {
|
||||
let Some(addr) = self.signup_address() else {
|
||||
self.fail("enter user and host".into());
|
||||
|
|
@ -1102,11 +1070,10 @@ impl Carrier {
|
|||
};
|
||||
return vec![PendingOp::Accept { address, identity }];
|
||||
}
|
||||
Message::CopyExport => {
|
||||
if let Some(text) = &self.backup_text {
|
||||
if let Some(display) = gtk::gdk::Display::default() {
|
||||
display.clipboard().set_text(text);
|
||||
}
|
||||
Message::CopySecret => {
|
||||
let text = ops::seed_text(&self.master());
|
||||
if let Some(display) = gtk::gdk::Display::default() {
|
||||
display.clipboard().set_text(&text);
|
||||
}
|
||||
}
|
||||
Message::RotatePressed => {
|
||||
|
|
@ -1129,20 +1096,14 @@ impl Carrier {
|
|||
self.dialog = None;
|
||||
return vec![PendingOp::Rotate];
|
||||
}
|
||||
Message::UnpinPressed(label) => {
|
||||
return vec![PendingOp::Unpin { label }];
|
||||
Message::ExportBackupTo(path) => {
|
||||
return vec![PendingOp::Export { path }];
|
||||
}
|
||||
Message::ExportPressed => {
|
||||
let dir = self.dir.clone();
|
||||
return vec![PendingOp::Export { dir }];
|
||||
}
|
||||
Message::ImportPressed => {
|
||||
if self.import_body.trim().is_empty() {
|
||||
Message::ImportBackupFrom(text) => {
|
||||
if text.trim().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
return vec![PendingOp::ImportBackup {
|
||||
text: self.import_body.clone(),
|
||||
}];
|
||||
return vec![PendingOp::ImportBackup { text }];
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
|
|
@ -1211,17 +1172,6 @@ impl Carrier {
|
|||
self.fail(e);
|
||||
None
|
||||
}
|
||||
OpResult::Exported(Ok((note, text))) => {
|
||||
self.done();
|
||||
self.backup_text = Some(text);
|
||||
self.note(note);
|
||||
None
|
||||
}
|
||||
OpResult::Exported(Err(e)) => {
|
||||
self.done();
|
||||
self.fail(e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1280,6 +1230,33 @@ mod tests {
|
|||
assert!(!app.keep_mail);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_reveal_toggles() {
|
||||
let mut app = app();
|
||||
assert!(!app.secret_revealed);
|
||||
app.handle(Message::SecretRevealToggled);
|
||||
assert!(app.secret_revealed);
|
||||
app.handle(Message::SecretRevealToggled);
|
||||
assert!(!app.secret_revealed);
|
||||
}
|
||||
|
||||
/// Export/Import now go through a native file dialog rather than a
|
||||
/// paste box, so the model only ever sees the chosen path/text —
|
||||
/// an empty import (e.g. an empty file) is a no-op, like before.
|
||||
#[test]
|
||||
fn export_and_import_dispatch_on_the_chosen_path() {
|
||||
let mut app = app();
|
||||
let path = std::path::PathBuf::from("/tmp/wherever.gsmol");
|
||||
let ops = app.handle(Message::ExportBackupTo(path.clone()));
|
||||
assert!(matches!(ops.as_slice(), [PendingOp::Export { path: p }] if *p == path));
|
||||
|
||||
assert!(app
|
||||
.handle(Message::ImportBackupFrom(" ".into()))
|
||||
.is_empty());
|
||||
let ops = app.handle(Message::ImportBackupFrom("some backup text".into()));
|
||||
assert!(matches!(ops.as_slice(), [PendingOp::ImportBackup { .. }]));
|
||||
}
|
||||
|
||||
/// A factory reset asks first, then erases every file in the config
|
||||
/// directory and returns the model to onboarding, as if the app had
|
||||
/// never run.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue