feat: rework signup into a safer multi-step flow, add keep-mail setting
This commit is contained in:
parent
53c9c4b8ce
commit
2892ea3fa7
3 changed files with 649 additions and 132 deletions
354
src/view.rs
354
src/view.rs
|
|
@ -19,7 +19,9 @@ use relm4::adw::prelude::*;
|
|||
use relm4::ComponentSender;
|
||||
use relm4::{Component, ComponentParts};
|
||||
|
||||
use crate::app::{Carrier, Dialog, Message, OnboardMode, OpResult, Page, PendingOp};
|
||||
use crate::app::{
|
||||
Carrier, Dialog, Message, OnboardMode, OpResult, Page, PendingOp, SignupMode, SignupStep,
|
||||
};
|
||||
use crate::ops::{self, Contact, Folder, Row};
|
||||
use relm4::RelmApp;
|
||||
|
||||
|
|
@ -193,6 +195,7 @@ pub struct CarrierWidgets {
|
|||
mail_list: gtk::ListBox,
|
||||
list_empty: adw::StatusPage,
|
||||
list_empty_fetch: gtk::Button,
|
||||
list_empty_hint: gtk::Label,
|
||||
article_stack: gtk::Stack,
|
||||
article_subject: gtk::Label,
|
||||
article_meta: gtk::Label,
|
||||
|
|
@ -223,16 +226,29 @@ pub struct CarrierWidgets {
|
|||
offer_row: adw::ActionRow,
|
||||
pins_group: adw::PreferencesGroup,
|
||||
pin_rows: Vec<adw::ActionRow>,
|
||||
keep_mail_row: adw::SwitchRow,
|
||||
timeout_row: adw::SpinRow,
|
||||
backup_label: gtk::Label,
|
||||
|
||||
// onboarding
|
||||
onboard_seed_label: gtk::Label,
|
||||
onboard_seed_entry: adw::EntryRow,
|
||||
|
||||
// signup
|
||||
signup_back: gtk::Button,
|
||||
signup_offer_list: gtk::ListBox,
|
||||
signup_offer_row: adw::ActionRow,
|
||||
signup_key_button: gtk::Button,
|
||||
signup_key_card: gtk::Box,
|
||||
signup_key_value: gtk::Label,
|
||||
signup_key_fingerprint: gtk::Label,
|
||||
signup_key_hint: gtk::Label,
|
||||
signup_start_over: gtk::Button,
|
||||
signup_step_back: gtk::Button,
|
||||
signup_mode_new: gtk::ToggleButton,
|
||||
signup_mode_existing: gtk::ToggleButton,
|
||||
signup_host: adw::EntryRow,
|
||||
signup_user: adw::EntryRow,
|
||||
signup_invite: adw::EntryRow,
|
||||
register_button: gtk::Button,
|
||||
recall_button: gtk::Button,
|
||||
|
||||
// status bar and dialogs
|
||||
spinner: gtk::Spinner,
|
||||
|
|
@ -414,7 +430,18 @@ impl CarrierWidgets {
|
|||
let list_empty = adw::StatusPage::new();
|
||||
let list_empty_fetch = icon_label_button("mail-send-receive-symbolic", "Fetch", "pill");
|
||||
list_empty_fetch.set_halign(Align::Center);
|
||||
list_empty.set_child(Some(&list_empty_fetch));
|
||||
let list_empty_hint = label(
|
||||
"Fetching removes mail from the server once it's stored here.\n\
|
||||
Turn on \"Keep mail on the server\" in Settings to keep it there instead.",
|
||||
"dim caption",
|
||||
);
|
||||
list_empty_hint.set_wrap(true);
|
||||
list_empty_hint.set_justify(gtk::Justification::Center);
|
||||
list_empty_hint.set_halign(Align::Center);
|
||||
let list_empty_box = gtk::Box::new(Orientation::Vertical, 8);
|
||||
list_empty_box.append(&list_empty_fetch);
|
||||
list_empty_box.append(&list_empty_hint);
|
||||
list_empty.set_child(Some(&list_empty_box));
|
||||
let list_stack = gtk::Stack::new();
|
||||
list_stack.set_hhomogeneous(false);
|
||||
list_stack.set_vhomogeneous(false);
|
||||
|
|
@ -625,6 +652,14 @@ impl CarrierWidgets {
|
|||
.build();
|
||||
settings.add(&pins_group);
|
||||
|
||||
let fetch_group = adw::PreferencesGroup::builder().title("Fetching").build();
|
||||
let keep_mail_row = adw::SwitchRow::builder()
|
||||
.title("Keep mail on the server")
|
||||
.subtitle("Off: a fetch removes mail from the server once it's stored here.")
|
||||
.build();
|
||||
fetch_group.add(&keep_mail_row);
|
||||
settings.add(&fetch_group);
|
||||
|
||||
let network_group = adw::PreferencesGroup::builder().title("Network").build();
|
||||
let timeout_row = adw::SpinRow::with_range(1.0, 600.0, 1.0);
|
||||
timeout_row.set_title("Timeout");
|
||||
|
|
@ -678,6 +713,19 @@ impl CarrierWidgets {
|
|||
import_group.add(&import_backup);
|
||||
settings.add(&import_group);
|
||||
|
||||
// The reset closes the page list: nothing after it survives a
|
||||
// wipe, so it sits last, marked destructive like the delete.
|
||||
let reset_group = adw::PreferencesGroup::builder()
|
||||
.title("Factory reset")
|
||||
.description(
|
||||
"Erase the seed key, the sealed store and every sidecar on this device, and return to onboarding. Mail already on the server is not touched.",
|
||||
)
|
||||
.build();
|
||||
let reset_button = icon_label_button("user-trash-symbolic", "Factory reset", "danger");
|
||||
reset_button.set_valign(Align::Center);
|
||||
reset_group.set_header_suffix(Some(&reset_button));
|
||||
settings.add(&reset_group);
|
||||
|
||||
stack.add_named(&settings, Some("settings"));
|
||||
|
||||
// onboarding: three static modes, each a centred column
|
||||
|
|
@ -700,8 +748,8 @@ impl CarrierWidgets {
|
|||
tagline.set_halign(Align::Center);
|
||||
tagline.set_margin_bottom(12);
|
||||
onboard_start.append(&tagline);
|
||||
let create_button = button("Create an identity", "pill");
|
||||
let restore_button = button("Restore from a seed", "pill-quiet");
|
||||
let create_button = button("Create a new identity", "pill");
|
||||
let restore_button = button("Restore existing identity from seed", "pill-quiet");
|
||||
onboard_start.append(&create_button);
|
||||
onboard_start.append(&restore_button);
|
||||
stack.add_named(¢er(&onboard_start), Some("onboard"));
|
||||
|
|
@ -746,46 +794,84 @@ impl CarrierWidgets {
|
|||
onboard_restore.append(&restore_back);
|
||||
stack.add_named(¢er(&onboard_restore), Some("onboard-restore"));
|
||||
|
||||
// signup
|
||||
let signup = gtk::Box::new(Orientation::Vertical, 12);
|
||||
signup.append(&label("Set up the address", "subtitle"));
|
||||
let signup_group = adw::PreferencesGroup::builder()
|
||||
.description(
|
||||
"The invite token and server key are only needed when the operator published them.",
|
||||
)
|
||||
.build();
|
||||
let signup_user = adw::EntryRow::builder().title("User").build();
|
||||
// signup: step 1 — the server this address lives on
|
||||
let signup_server = gtk::Box::new(Orientation::Vertical, 12);
|
||||
signup_server.append(&label("Set up the server", "subtitle"));
|
||||
let signup_server_group = adw::PreferencesGroup::new();
|
||||
let signup_host = adw::EntryRow::builder().title("Host").build();
|
||||
let signup_invite = adw::EntryRow::builder().title("Invite token").build();
|
||||
let signup_key = adw::EntryRow::builder().title("Server key").build();
|
||||
for row in [&signup_user, &signup_host, &signup_invite, &signup_key] {
|
||||
signup_group.add(row);
|
||||
}
|
||||
signup.append(&signup_group);
|
||||
let signup_learn = button("Learn the server key and pin what it presents", "");
|
||||
signup.append(&signup_learn);
|
||||
let signup_offer_list = boxed_list();
|
||||
signup_offer_list.set_visible(false);
|
||||
let signup_offer_row = adw::ActionRow::builder()
|
||||
.title("Server key offered")
|
||||
signup_server_group.add(&signup_host);
|
||||
signup_server.append(&signup_server_group);
|
||||
|
||||
// Shown once a key has been fetched: the key and its fingerprint,
|
||||
// prominent, plus the reminder to check it against what the
|
||||
// operator published. Hidden otherwise.
|
||||
let signup_key_value = label("", "mono");
|
||||
signup_key_value.set_selectable(true);
|
||||
signup_key_value.set_wrap(true);
|
||||
signup_key_value.set_margin_top(12);
|
||||
signup_key_value.set_margin_start(12);
|
||||
signup_key_value.set_margin_end(12);
|
||||
let signup_key_fingerprint = label("", "mono dim");
|
||||
signup_key_fingerprint.set_selectable(true);
|
||||
signup_key_fingerprint.set_wrap(true);
|
||||
signup_key_fingerprint.set_margin_bottom(12);
|
||||
signup_key_fingerprint.set_margin_start(12);
|
||||
signup_key_fingerprint.set_margin_end(12);
|
||||
let signup_key_card = gtk::Box::new(Orientation::Vertical, 4);
|
||||
signup_key_card.add_css_class("card");
|
||||
signup_key_card.set_visible(false);
|
||||
signup_key_card.append(&signup_key_value);
|
||||
signup_key_card.append(&signup_key_fingerprint);
|
||||
signup_server.append(&signup_key_card);
|
||||
let signup_key_hint = label(
|
||||
"If the server operator published a key or fingerprint, make \
|
||||
sure it matches before trusting it.",
|
||||
"dim",
|
||||
);
|
||||
signup_key_hint.set_wrap(true);
|
||||
signup_key_hint.set_visible(false);
|
||||
signup_server.append(&signup_key_hint);
|
||||
|
||||
let signup_key_button = button("Fetch the server's key", "pill");
|
||||
let signup_start_over = button("Start over", "pill-quiet");
|
||||
signup_server.append(&signup_key_button);
|
||||
signup_server.append(&signup_start_over);
|
||||
stack.add_named(¢er(&signup_server), Some("signup-server"));
|
||||
|
||||
// signup: step 2 — bind a username, or recall an existing address
|
||||
let signup_account = gtk::Box::new(Orientation::Vertical, 12);
|
||||
signup_account.append(&label("Choose your address", "subtitle"));
|
||||
let signup_mode_new = gtk::ToggleButton::builder()
|
||||
.label("Create a new address")
|
||||
.active(true)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
signup_offer_row.set_subtitle_lines(2);
|
||||
let signup_offer_pin = button("Pin", "accent");
|
||||
signup_offer_pin.set_valign(Align::Center);
|
||||
let signup_offer_discard = button("Discard", "");
|
||||
signup_offer_discard.set_valign(Align::Center);
|
||||
signup_offer_row.add_suffix(&signup_offer_discard);
|
||||
signup_offer_row.add_suffix(&signup_offer_pin);
|
||||
signup_offer_list.append(&signup_offer_row);
|
||||
signup.append(&signup_offer_list);
|
||||
let signup_mode_existing = gtk::ToggleButton::builder()
|
||||
.label("I already have this address")
|
||||
.hexpand(true)
|
||||
.build();
|
||||
signup_mode_existing.set_group(Some(&signup_mode_new));
|
||||
let signup_mode_box = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
signup_mode_box.add_css_class("linked");
|
||||
signup_mode_box.set_homogeneous(true);
|
||||
signup_mode_box.append(&signup_mode_new);
|
||||
signup_mode_box.append(&signup_mode_existing);
|
||||
signup_account.append(&signup_mode_box);
|
||||
let signup_account_group = adw::PreferencesGroup::new();
|
||||
let signup_user = adw::EntryRow::builder().title("User").build();
|
||||
let signup_invite = adw::EntryRow::builder()
|
||||
.title("Invite token (if required by the server)")
|
||||
.build();
|
||||
signup_account_group.add(&signup_user);
|
||||
signup_account_group.add(&signup_invite);
|
||||
signup_account.append(&signup_account_group);
|
||||
let register_button = button("Register", "pill");
|
||||
let recall_button = button("I already have this address", "pill-quiet");
|
||||
signup.append(®ister_button);
|
||||
signup.append(&recall_button);
|
||||
let signup_back = button("Back", "quiet");
|
||||
signup_back.set_visible(false);
|
||||
signup.append(&signup_back);
|
||||
stack.add_named(¢er(&signup), Some("signup"));
|
||||
let recall_button = button("Restore access", "pill");
|
||||
signup_account.append(®ister_button);
|
||||
signup_account.append(&recall_button);
|
||||
let signup_step_back = button("Back", "quiet");
|
||||
signup_account.append(&signup_step_back);
|
||||
stack.add_named(¢er(&signup_account), Some("signup-account"));
|
||||
|
||||
// --- status bar -------------------------------------------------
|
||||
// One fixed line at the bottom: the spinner and the running
|
||||
|
|
@ -939,17 +1025,20 @@ impl CarrierWidgets {
|
|||
});
|
||||
|
||||
rotate_button.connect_clicked(input(Message::RotatePressed));
|
||||
reset_button.connect_clicked(input(Message::ResetPressed));
|
||||
learn_button.connect_clicked(input(Message::SettingsLearnKey));
|
||||
offer_pin.connect_clicked(input(Message::PinAccept));
|
||||
signup_offer_pin.connect_clicked(input(Message::PinAccept));
|
||||
offer_discard.connect_clicked(input(Message::PinDiscard));
|
||||
signup_offer_discard.connect_clicked(input(Message::PinDiscard));
|
||||
export_button.connect_clicked(input(Message::ExportPressed));
|
||||
import_backup.connect_clicked(input(Message::ImportPressed));
|
||||
let timeout_sender = sender.clone();
|
||||
timeout_row.connect_value_notify(move |row| {
|
||||
timeout_sender.input(Message::TimeoutChanged((row.value() as u64).to_string()))
|
||||
});
|
||||
let keep_mail_sender = sender.clone();
|
||||
keep_mail_row.connect_active_notify(move |row| {
|
||||
keep_mail_sender.input(Message::KeepMailChanged(row.is_active()))
|
||||
});
|
||||
let import_body_sender = sender.clone();
|
||||
import_body.connect_changed(move |buffer| {
|
||||
import_body_sender.input(Message::ImportBodyChanged(buffer_text(buffer)))
|
||||
|
|
@ -982,14 +1071,23 @@ impl CarrierWidgets {
|
|||
signup_invite.connect_changed(move |entry| {
|
||||
invite_sender.input(Message::SignupInviteChanged(entry.text().to_string()))
|
||||
});
|
||||
let key_sender = sender.clone();
|
||||
signup_key.connect_changed(move |entry| {
|
||||
key_sender.input(Message::SignupKeyChanged(entry.text().to_string()))
|
||||
});
|
||||
signup_learn.connect_clicked(input(Message::SignupLearnKey));
|
||||
signup_key_button.connect_clicked(input(Message::SignupKeyAction));
|
||||
signup_start_over.connect_clicked(input(Message::SignupStartOver));
|
||||
signup_step_back.connect_clicked(input(Message::SignupStepBack));
|
||||
register_button.connect_clicked(input(Message::SignupRegister));
|
||||
recall_button.connect_clicked(input(Message::SignupRecall));
|
||||
signup_back.connect_clicked(input(Message::SignupBack));
|
||||
let mode_new_sender = sender.clone();
|
||||
signup_mode_new.connect_toggled(move |btn| {
|
||||
if btn.is_active() {
|
||||
mode_new_sender.input(Message::SignupModeChanged(SignupMode::New));
|
||||
}
|
||||
});
|
||||
let mode_existing_sender = sender.clone();
|
||||
signup_mode_existing.connect_toggled(move |btn| {
|
||||
if btn.is_active() {
|
||||
mode_existing_sender.input(Message::SignupModeChanged(SignupMode::Existing));
|
||||
}
|
||||
});
|
||||
|
||||
CarrierWidgets {
|
||||
window: root.clone(),
|
||||
|
|
@ -1005,6 +1103,7 @@ impl CarrierWidgets {
|
|||
mail_list,
|
||||
list_empty,
|
||||
list_empty_fetch,
|
||||
list_empty_hint,
|
||||
article_stack,
|
||||
article_subject,
|
||||
article_meta,
|
||||
|
|
@ -1027,12 +1126,25 @@ impl CarrierWidgets {
|
|||
offer_row,
|
||||
pins_group,
|
||||
pin_rows: Vec::new(),
|
||||
keep_mail_row,
|
||||
timeout_row,
|
||||
backup_label,
|
||||
onboard_seed_label,
|
||||
signup_back,
|
||||
signup_offer_list,
|
||||
signup_offer_row,
|
||||
onboard_seed_entry,
|
||||
signup_key_button,
|
||||
signup_key_card,
|
||||
signup_key_value,
|
||||
signup_key_fingerprint,
|
||||
signup_key_hint,
|
||||
signup_start_over,
|
||||
signup_step_back,
|
||||
signup_mode_new,
|
||||
signup_mode_existing,
|
||||
signup_host,
|
||||
signup_user,
|
||||
signup_invite,
|
||||
register_button,
|
||||
recall_button,
|
||||
spinner,
|
||||
status_label,
|
||||
cancel_button,
|
||||
|
|
@ -1062,10 +1174,15 @@ impl Component for Carrier {
|
|||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let (model, op) = Carrier::boot(init);
|
||||
let widgets = CarrierWidgets::new(&root, &sender);
|
||||
let mut widgets = CarrierWidgets::new(&root, &sender);
|
||||
if let Some(op) = op {
|
||||
spawn_op(&model, op, &sender);
|
||||
}
|
||||
// `update_view` runs only after a message, and a clean install
|
||||
// boots with none queued, so the first paint happens here —
|
||||
// otherwise the stack sits on its default page until the first
|
||||
// click flips it to the page the model opened with.
|
||||
render(&model, &mut widgets, &sender);
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
|
|
@ -1098,21 +1215,15 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender<Carrier>) {
|
|||
let store = model.store.clone();
|
||||
let master = model.master.unwrap_or([0u8; KEY_LEN]);
|
||||
let timeout = model.timeout;
|
||||
let keep_mail = model.keep_mail;
|
||||
match op {
|
||||
PendingOp::Snapshot { read } => sender.spawn_command(move |out| {
|
||||
let _ = out.send(OpResult::SnapshotLoaded(ops::snapshot(
|
||||
&store, &master, &read,
|
||||
)));
|
||||
}),
|
||||
PendingOp::Register { addr, invite, key } => sender.spawn_command(move |out| {
|
||||
let res = if key.is_empty() {
|
||||
ops::register(&store, &master, &addr, invite.as_deref(), timeout)
|
||||
} else {
|
||||
ops::parse_key(&key).and_then(|key| {
|
||||
ops::pin(&store, &addr, &key)?;
|
||||
ops::register(&store, &master, &addr, invite.as_deref(), timeout)
|
||||
})
|
||||
};
|
||||
PendingOp::Register { addr, invite } => sender.spawn_command(move |out| {
|
||||
let res = ops::register(&store, &master, &addr, invite.as_deref(), timeout);
|
||||
let _ = out.send(OpResult::Generic(res.map(|_| "registered".to_string())));
|
||||
}),
|
||||
PendingOp::Restore { addr } => sender.spawn_command(move |out| {
|
||||
|
|
@ -1142,7 +1253,7 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender<Carrier>) {
|
|||
});
|
||||
sender.spawn_command(move |out| {
|
||||
let _ = out.send(OpResult::Generic(ops::fetch(
|
||||
&store, &master, cancel, timeout,
|
||||
&store, &master, cancel, keep_mail, timeout,
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
|
@ -1321,7 +1432,10 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender<Carr
|
|||
OnboardMode::Created => "onboard-created",
|
||||
OnboardMode::Restore => "onboard-restore",
|
||||
},
|
||||
Page::Signup => "signup",
|
||||
Page::Signup => match model.signup.step {
|
||||
SignupStep::Server => "signup-server",
|
||||
SignupStep::Account => "signup-account",
|
||||
},
|
||||
Page::Mail => "mail",
|
||||
Page::Settings => "settings",
|
||||
}
|
||||
|
|
@ -1332,32 +1446,30 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender<Carr
|
|||
clear_list(&w.mail_list);
|
||||
let rows = model.rows();
|
||||
let (icon, title, description) = match model.folder {
|
||||
Folder::Inbox => (
|
||||
"mail-read-symbolic",
|
||||
"Inbox is empty",
|
||||
"Fetch to check the mailbox.",
|
||||
),
|
||||
Folder::Inbox => (None, "Inbox is empty", "Fetch to check the mailbox."),
|
||||
Folder::Requests => (
|
||||
"contact-new-symbolic",
|
||||
Some("contact-new-symbolic"),
|
||||
"No requests",
|
||||
"Mail from senders you have not accepted yet arrives here.",
|
||||
),
|
||||
Folder::Sent => (
|
||||
"mail-send-symbolic",
|
||||
Some("mail-send-symbolic"),
|
||||
"Nothing sent yet",
|
||||
"Messages you send keep a sealed copy here.",
|
||||
),
|
||||
Folder::Drafts => (
|
||||
"document-edit-symbolic",
|
||||
Some("document-edit-symbolic"),
|
||||
"No drafts",
|
||||
"A compose closed from the sidebar is kept here.",
|
||||
),
|
||||
};
|
||||
w.list_empty.set_icon_name(Some(icon));
|
||||
w.list_empty.set_icon_name(icon);
|
||||
w.list_empty.set_title(title);
|
||||
w.list_empty.set_description(Some(description));
|
||||
w.list_empty_fetch
|
||||
.set_visible(model.folder == Folder::Inbox);
|
||||
let inbox_empty = model.folder == Folder::Inbox;
|
||||
w.list_empty_fetch.set_visible(inbox_empty);
|
||||
w.list_empty_hint
|
||||
.set_visible(inbox_empty && !model.keep_mail);
|
||||
w.list_stack
|
||||
.set_visible_child_name(if rows.is_empty() { "empty" } else { "list" });
|
||||
let mut selected_row = None;
|
||||
|
|
@ -1594,11 +1706,11 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender<Carr
|
|||
w.offer_row.set_title(&escape(&fingerprint(key)));
|
||||
w.offer_row.set_subtitle(&escape(&b32(key)));
|
||||
w.offer_group.set_visible(true);
|
||||
w.signup_offer_row.set_subtitle(&escape(&fingerprint(key)));
|
||||
w.signup_offer_list.set_visible(true);
|
||||
} else {
|
||||
w.offer_group.set_visible(false);
|
||||
w.signup_offer_list.set_visible(false);
|
||||
}
|
||||
if w.keep_mail_row.is_active() != model.keep_mail {
|
||||
w.keep_mail_row.set_active(model.keep_mail);
|
||||
}
|
||||
if w.timeout_row.value() as u64 != model.timeout {
|
||||
w.timeout_row.set_value(model.timeout as f64);
|
||||
|
|
@ -1635,13 +1747,60 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender<Carr
|
|||
.unwrap_or_default();
|
||||
w.backup_label.set_text(&backup_text);
|
||||
|
||||
// A programmatic reset (factory reset, discarding a cancelled
|
||||
// identity) clears these in the model; push that once into the
|
||||
// entries too, the same way a compose prefill does, so typing from
|
||||
// a previous attempt cannot linger on screen.
|
||||
if model.form_fresh.take() {
|
||||
w.onboard_seed_entry.set_text(&model.onboard.seed);
|
||||
w.signup_host.set_text(&model.signup.host);
|
||||
w.signup_user.set_text(&model.signup.user);
|
||||
w.signup_invite.set_text(&model.signup.invite);
|
||||
}
|
||||
|
||||
// Onboarding.
|
||||
if model.onboard.mode == OnboardMode::Created {
|
||||
w.onboard_seed_label.set_text(&model.onboard.seed_shown);
|
||||
}
|
||||
|
||||
// Signup.
|
||||
w.signup_back.set_visible(model.signup.fresh);
|
||||
let signup_busy = model.busy.is_none();
|
||||
for button in [
|
||||
&w.signup_key_button,
|
||||
&w.signup_start_over,
|
||||
&w.signup_step_back,
|
||||
&w.register_button,
|
||||
&w.recall_button,
|
||||
] {
|
||||
button.set_sensitive(signup_busy);
|
||||
}
|
||||
// The server step's one button: a fetched or already-trusted key
|
||||
// reads "Continue" and shows the key prominently; otherwise "Fetch".
|
||||
if let Some((_, key)) = &model.pin_offer {
|
||||
w.signup_key_value.set_text(&b32(key));
|
||||
w.signup_key_fingerprint.set_text(&fingerprint(key));
|
||||
w.signup_key_card.set_visible(true);
|
||||
w.signup_key_hint.set_visible(true);
|
||||
w.signup_key_button.set_label("Continue");
|
||||
} else if model.signup.key_trusted {
|
||||
w.signup_key_card.set_visible(false);
|
||||
w.signup_key_hint.set_visible(false);
|
||||
w.signup_key_button.set_label("Continue");
|
||||
} else {
|
||||
w.signup_key_card.set_visible(false);
|
||||
w.signup_key_hint.set_visible(false);
|
||||
w.signup_key_button.set_label("Fetch the server's key");
|
||||
}
|
||||
let new_address = model.signup.mode == SignupMode::New;
|
||||
w.signup_invite.set_visible(new_address);
|
||||
w.register_button.set_visible(new_address);
|
||||
w.recall_button.set_visible(!new_address);
|
||||
if w.signup_mode_new.is_active() != new_address {
|
||||
w.signup_mode_new.set_active(new_address);
|
||||
}
|
||||
if w.signup_mode_existing.is_active() == new_address {
|
||||
w.signup_mode_existing.set_active(!new_address);
|
||||
}
|
||||
|
||||
// Confirmations are presented once, on the None -> Some edge: `render`
|
||||
// runs after every message and an alert dialog is modal, so presenting
|
||||
|
|
@ -1650,6 +1809,7 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender<Carr
|
|||
(Some(dialog), false) => {
|
||||
let rotating = matches!(dialog, Dialog::Rotate);
|
||||
let replying = matches!(dialog, Dialog::Reply { .. });
|
||||
let resetting = matches!(dialog, Dialog::Reset);
|
||||
let alert = match dialog {
|
||||
Dialog::Delete { kept, .. } => {
|
||||
let alert = adw::AlertDialog::new(
|
||||
|
|
@ -1682,6 +1842,16 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender<Carr
|
|||
alert.set_response_appearance("rotate", adw::ResponseAppearance::Destructive);
|
||||
alert
|
||||
}
|
||||
Dialog::Reset => {
|
||||
let alert = adw::AlertDialog::new(
|
||||
Some("Factory reset this device?"),
|
||||
Some("The seed key, the sealed store, contacts, server pins, drafts and read marks are erased here. Mail already on the server stays there — without the seed it cannot be read back."),
|
||||
);
|
||||
alert.add_response("cancel", "Cancel");
|
||||
alert.add_response("reset", "Factory reset");
|
||||
alert.set_response_appearance("reset", adw::ResponseAppearance::Destructive);
|
||||
alert
|
||||
}
|
||||
Dialog::Reply { rows, .. } => {
|
||||
let list = rows
|
||||
.iter()
|
||||
|
|
@ -1708,15 +1878,17 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender<Carr
|
|||
alert.set_default_response(Some(close_default));
|
||||
let response_sender = sender.clone();
|
||||
alert.connect_response(None, move |_, response| {
|
||||
response_sender.input(match (rotating, replying, response) {
|
||||
(true, _, "rotate") => Message::RotateConfirmed,
|
||||
(true, _, _) => Message::RotateCancelled,
|
||||
(_, true, "copy") => Message::ReplyConfirmed(true),
|
||||
(_, true, "without") => Message::ReplyConfirmed(false),
|
||||
(_, true, _) => Message::ReplyCancelled,
|
||||
(false, _, "here") => Message::DeleteConfirmed(false),
|
||||
(false, _, "everywhere") => Message::DeleteConfirmed(true),
|
||||
(false, _, _) => Message::DeleteCancelled,
|
||||
response_sender.input(match (rotating, replying, resetting, response) {
|
||||
(true, _, _, "rotate") => Message::RotateConfirmed,
|
||||
(true, _, _, _) => Message::RotateCancelled,
|
||||
(_, true, _, "copy") => Message::ReplyConfirmed(true),
|
||||
(_, true, _, "without") => Message::ReplyConfirmed(false),
|
||||
(_, true, _, _) => Message::ReplyCancelled,
|
||||
(_, _, true, "reset") => Message::ResetConfirmed,
|
||||
(_, _, true, _) => Message::ResetCancelled,
|
||||
(false, _, _, "here") => Message::DeleteConfirmed(false),
|
||||
(false, _, _, "everywhere") => Message::DeleteConfirmed(true),
|
||||
(false, _, _, _) => Message::DeleteCancelled,
|
||||
});
|
||||
});
|
||||
alert.present(Some(&w.window));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue