Build 78: honest transport labels, request decline/cancel, NIP-05 on requests, full localization
Settings now says "Manual transaction" and the privacy row reads "Messages & lookups" opening a new Network privacy page that tells the truth: messages, names, price and avatars ride the Nym mixnet; the grin node connects directly. README and lander updated to match. Requests are messages, payments are final: declining a request now sends the requester a void control message (NIP-17), a requester can cancel a request they sent (cancels the local invoice and notifies the payer), and incoming requests resolve the sender's verified @username instead of a bare npub. The Requested amount on the success screen is centered. New NostrDecline/NostrCancel tasks and a goblin-action control message carry it, bound to the stored counterparty. Localization: every Goblin-screen string moved to t!() keys (370 keys) and translated into de/fr/ru/tr/zh-CN, guarded by a key/placeholder drift test. System-locale auto-detect now matches region locales like zh-CN.
This commit is contained in:
+564
-345
File diff suppressed because it is too large
Load Diff
+119
-101
@@ -185,22 +185,18 @@ impl OnboardingContent {
|
||||
);
|
||||
});
|
||||
ui.add_space(26.0);
|
||||
let lines: [(&str, &str); 3] = [
|
||||
let lines: [(String, String); 3] = [
|
||||
(
|
||||
"Private money",
|
||||
"Goblin is a wallet for grin — digital cash with no amounts \
|
||||
or addresses on its chain.",
|
||||
t!("goblin.onboarding.intro.private_money_head").to_string(),
|
||||
t!("goblin.onboarding.intro.private_money_body").to_string(),
|
||||
),
|
||||
(
|
||||
"Send like a message",
|
||||
"Pay a @username or npub and it arrives as an end-to-end \
|
||||
encrypted message over nostr and the Nym mixnet — no one in \
|
||||
between can see the amount or who's involved.",
|
||||
t!("goblin.onboarding.intro.send_like_message_head").to_string(),
|
||||
t!("goblin.onboarding.intro.send_like_message_body").to_string(),
|
||||
),
|
||||
(
|
||||
"Yours alone",
|
||||
"Keys, names and history live on this device. Built on the \
|
||||
GRIM wallet.",
|
||||
t!("goblin.onboarding.intro.yours_alone_head").to_string(),
|
||||
t!("goblin.onboarding.intro.yours_alone_body").to_string(),
|
||||
),
|
||||
];
|
||||
for (head, body) in lines {
|
||||
@@ -221,13 +217,13 @@ impl OnboardingContent {
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
if w::big_action(ui, "Get started", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.onboarding.intro.get_started"), false).clicked() {
|
||||
self.step = Step::Node;
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("Takes about a minute. You can change everything later.")
|
||||
RichText::new(t!("goblin.onboarding.intro.footnote"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -293,17 +289,16 @@ impl OnboardingContent {
|
||||
let t = theme::tokens();
|
||||
self.step_header(
|
||||
ui,
|
||||
"STEP 1 OF 3 · NETWORK",
|
||||
"How should Goblin\nwatch the chain?",
|
||||
&t!("goblin.onboarding.node.kicker"),
|
||||
&t!("goblin.onboarding.node.title"),
|
||||
Step::Intro,
|
||||
);
|
||||
if Self::node_card(
|
||||
ui,
|
||||
self.integrated,
|
||||
"Run my own node",
|
||||
"Private",
|
||||
"Trusts no one — your wallet checks the chain itself. Syncs in \
|
||||
the background while you finish setup.",
|
||||
&t!("goblin.onboarding.node.own_title"),
|
||||
&t!("goblin.onboarding.node.own_badge"),
|
||||
&t!("goblin.onboarding.node.own_body"),
|
||||
) {
|
||||
self.integrated = true;
|
||||
}
|
||||
@@ -311,9 +306,9 @@ impl OnboardingContent {
|
||||
if Self::node_card(
|
||||
ui,
|
||||
!self.integrated,
|
||||
"Connect to a node",
|
||||
"Instant",
|
||||
"No sync wait. The node you pick can see your wallet's queries.",
|
||||
&t!("goblin.onboarding.node.connect_title"),
|
||||
&t!("goblin.onboarding.node.connect_badge"),
|
||||
&t!("goblin.onboarding.node.connect_body"),
|
||||
) {
|
||||
self.integrated = false;
|
||||
}
|
||||
@@ -330,7 +325,7 @@ impl OnboardingContent {
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("Changeable any time in Settings → Node.")
|
||||
RichText::new(t!("goblin.onboarding.node.changeable"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -338,13 +333,13 @@ impl OnboardingContent {
|
||||
let url_ok = self.integrated
|
||||
|| self.ext_url.trim().starts_with("http://")
|
||||
|| self.ext_url.trim().starts_with("https://");
|
||||
if w::big_action(ui, "Continue", false).clicked() && url_ok {
|
||||
if w::big_action(ui, &t!("goblin.onboarding.node.continue"), false).clicked() && url_ok {
|
||||
self.step = Step::WalletSetup;
|
||||
}
|
||||
if !url_ok {
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("Node URL must start with http:// or https://")
|
||||
RichText::new(t!("goblin.onboarding.node.url_invalid"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
@@ -355,12 +350,20 @@ impl OnboardingContent {
|
||||
|
||||
fn wallet_setup_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
self.step_header(ui, "STEP 2 OF 3 · WALLET", "Set up your wallet", Step::Node);
|
||||
self.step_header(
|
||||
ui,
|
||||
&t!("goblin.onboarding.wallet.kicker"),
|
||||
&t!("goblin.onboarding.wallet.title"),
|
||||
Step::Node,
|
||||
);
|
||||
|
||||
// Create / Restore segmented choice.
|
||||
ui.horizontal(|ui| {
|
||||
let half = (ui.available_width() - 10.0) / 2.0;
|
||||
for (restore, label) in [(false, "Create new"), (true, "Restore from seed")] {
|
||||
for (restore, label) in [
|
||||
(false, t!("goblin.onboarding.wallet.create_new")),
|
||||
(true, t!("goblin.onboarding.wallet.restore_from_seed")),
|
||||
] {
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
|
||||
ui.cursor().min,
|
||||
@@ -368,7 +371,7 @@ impl OnboardingContent {
|
||||
)),
|
||||
|ui| {
|
||||
let active = self.restore == restore;
|
||||
let resp = w::chip(ui, label, active);
|
||||
let resp = w::chip(ui, &label, active);
|
||||
if resp.clicked() {
|
||||
self.restore = restore;
|
||||
}
|
||||
@@ -382,7 +385,7 @@ impl OnboardingContent {
|
||||
w::field_well(ui, |ui| {
|
||||
TextEdit::new(egui::Id::from("onb_name"))
|
||||
.focus(false)
|
||||
.hint_text("Wallet name")
|
||||
.hint_text(t!("goblin.onboarding.wallet.name_hint"))
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.ui(ui, &mut self.name, cb);
|
||||
@@ -391,7 +394,7 @@ impl OnboardingContent {
|
||||
w::field_well(ui, |ui| {
|
||||
TextEdit::new(egui::Id::from("onb_pass"))
|
||||
.focus(false)
|
||||
.hint_text("Password")
|
||||
.hint_text(t!("goblin.onboarding.wallet.password_hint"))
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
@@ -401,7 +404,7 @@ impl OnboardingContent {
|
||||
w::field_well(ui, |ui| {
|
||||
TextEdit::new(egui::Id::from("onb_pass2"))
|
||||
.focus(false)
|
||||
.hint_text("Repeat password")
|
||||
.hint_text(t!("goblin.onboarding.wallet.repeat_password_hint"))
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
@@ -410,10 +413,9 @@ impl OnboardingContent {
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
RichText::new(if self.restore {
|
||||
"Have your seed words ready — you'll enter them next."
|
||||
t!("goblin.onboarding.wallet.restore_hint")
|
||||
} else {
|
||||
"Next you'll get 24 seed words to write down. They are the \
|
||||
money — anyone holding them holds your funds."
|
||||
t!("goblin.onboarding.wallet.create_hint")
|
||||
})
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
@@ -422,7 +424,10 @@ impl OnboardingContent {
|
||||
|
||||
let pass_ok = !self.pass.is_empty() && self.pass == self.pass2;
|
||||
let name_ok = !self.name.trim().is_empty();
|
||||
if w::big_action(ui, "Continue", false).clicked() && pass_ok && name_ok {
|
||||
if w::big_action(ui, &t!("goblin.onboarding.wallet.continue"), false).clicked()
|
||||
&& pass_ok
|
||||
&& name_ok
|
||||
{
|
||||
self.mnemonic_setup.reset();
|
||||
self.mnemonic_setup.mnemonic.set_mode(if self.restore {
|
||||
PhraseMode::Import
|
||||
@@ -436,7 +441,7 @@ impl OnboardingContent {
|
||||
if !self.pass.is_empty() && self.pass != self.pass2 {
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("Passwords don't match")
|
||||
RichText::new(t!("goblin.onboarding.wallet.passwords_no_match"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
@@ -453,14 +458,15 @@ impl OnboardingContent {
|
||||
) {
|
||||
let t = theme::tokens();
|
||||
let restore = self.mnemonic_setup.mnemonic.mode() == PhraseMode::Import;
|
||||
let words_title = if restore {
|
||||
t!("goblin.onboarding.words.title_restore")
|
||||
} else {
|
||||
t!("goblin.onboarding.words.title_create")
|
||||
};
|
||||
self.step_header(
|
||||
ui,
|
||||
"STEP 2 OF 3 · WALLET",
|
||||
if restore {
|
||||
"Enter your seed words"
|
||||
} else {
|
||||
"Write these words down"
|
||||
},
|
||||
&t!("goblin.onboarding.words.kicker"),
|
||||
&words_title,
|
||||
Step::WalletSetup,
|
||||
);
|
||||
if restore {
|
||||
@@ -478,12 +484,9 @@ impl OnboardingContent {
|
||||
ui.add_space(10.0);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"On paper, in order. Anyone with these words can take \
|
||||
your funds; without them a lost device means lost funds.",
|
||||
)
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
RichText::new(t!("goblin.onboarding.words.write_down_hint"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
@@ -501,7 +504,7 @@ impl OnboardingContent {
|
||||
Vec2::new(half, 44.0),
|
||||
)),
|
||||
|ui| {
|
||||
if w::chip(ui, "Paste", false).clicked() {
|
||||
if w::chip(ui, &t!("goblin.onboarding.words.paste"), false).clicked() {
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||
self.mnemonic_setup.mnemonic.import(&data);
|
||||
}
|
||||
@@ -514,7 +517,7 @@ impl OnboardingContent {
|
||||
Vec2::new(half, 44.0),
|
||||
)),
|
||||
|ui| {
|
||||
if w::chip(ui, "Scan QR", false).clicked() {
|
||||
if w::chip(ui, &t!("goblin.onboarding.words.scan_qr"), false).clicked() {
|
||||
self.scan_modal = Some(CameraScanContent::default());
|
||||
Modal::new(OB_PHRASE_SCAN_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
@@ -527,7 +530,7 @@ impl OnboardingContent {
|
||||
);
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
} else if w::chip(ui, "Copy to clipboard (avoid this)", false).clicked() {
|
||||
} else if w::chip(ui, &t!("goblin.onboarding.words.copy_clipboard"), false).clicked() {
|
||||
cb.copy_string_to_buffer(self.mnemonic_setup.mnemonic.get_phrase());
|
||||
}
|
||||
if !restore {
|
||||
@@ -540,12 +543,12 @@ impl OnboardingContent {
|
||||
true
|
||||
};
|
||||
let label = if restore {
|
||||
"Restore wallet"
|
||||
t!("goblin.onboarding.words.restore_wallet")
|
||||
} else {
|
||||
"I wrote them down"
|
||||
t!("goblin.onboarding.words.wrote_them_down")
|
||||
};
|
||||
if ready {
|
||||
if w::big_action(ui, label, false).clicked() {
|
||||
if w::big_action(ui, &label, false).clicked() {
|
||||
if restore {
|
||||
self.create_wallet(wallets);
|
||||
} else {
|
||||
@@ -554,7 +557,7 @@ impl OnboardingContent {
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Fill every word — tap a word to edit it, or paste the phrase.")
|
||||
RichText::new(t!("goblin.onboarding.words.fill_every_word"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -569,27 +572,32 @@ impl OnboardingContent {
|
||||
cb: &dyn PlatformCallbacks,
|
||||
) {
|
||||
let t = theme::tokens();
|
||||
self.step_header(ui, "STEP 2 OF 3 · WALLET", "Now prove it", Step::Words);
|
||||
self.step_header(
|
||||
ui,
|
||||
&t!("goblin.onboarding.confirm.kicker"),
|
||||
&t!("goblin.onboarding.confirm.title"),
|
||||
Step::Words,
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Enter the words you just wrote down. Tap a word to type it.")
|
||||
RichText::new(t!("goblin.onboarding.confirm.enter_hint"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
self.mnemonic_setup.word_list_ui(ui, true);
|
||||
ui.add_space(14.0);
|
||||
if w::chip(ui, "Paste", false).clicked() {
|
||||
if w::chip(ui, &t!("goblin.onboarding.confirm.paste"), false).clicked() {
|
||||
let data = ZeroingString::from(cb.get_string_from_buffer());
|
||||
self.mnemonic_setup.mnemonic.import(&data);
|
||||
}
|
||||
ui.add_space(14.0);
|
||||
if !self.mnemonic_setup.mnemonic.has_empty_or_invalid() {
|
||||
if w::big_action(ui, "Create wallet", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.onboarding.confirm.create_wallet"), false).clicked() {
|
||||
self.create_wallet(wallets);
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new("Keep going — every word, in order.")
|
||||
RichText::new(t!("goblin.onboarding.confirm.keep_going"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -650,10 +658,20 @@ impl OnboardingContent {
|
||||
self.error = None;
|
||||
self.step = Step::Identity;
|
||||
}
|
||||
Err(e) => self.error = Some(format!("Couldn't open the wallet: {:?}", e)),
|
||||
Err(e) => {
|
||||
self.error = Some(
|
||||
t!("goblin.onboarding.errors.cant_open", err => format!("{:?}", e))
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => self.error = Some(format!("Couldn't create the wallet: {:?}", e)),
|
||||
Err(e) => {
|
||||
self.error = Some(
|
||||
t!("goblin.onboarding.errors.cant_create", err => format!("{:?}", e))
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,13 +681,13 @@ impl OnboardingContent {
|
||||
let t = theme::tokens();
|
||||
// No back from here: the wallet exists now.
|
||||
ui.label(
|
||||
RichText::new("STEP 3 OF 3 · IDENTITY")
|
||||
RichText::new(t!("goblin.onboarding.identity.kicker"))
|
||||
.font(fonts::kicker())
|
||||
.color(t.text_mute),
|
||||
);
|
||||
ui.add_space(18.0);
|
||||
ui.label(
|
||||
RichText::new("Your payment identity")
|
||||
RichText::new(t!("goblin.onboarding.identity.title"))
|
||||
.font(FontId::new(26.0, fonts::bold()))
|
||||
.color(t.text),
|
||||
);
|
||||
@@ -698,7 +716,7 @@ impl OnboardingContent {
|
||||
let short = if npub.len() > 20 {
|
||||
format!("{}…{}", &npub[..12], &npub[npub.len() - 6..])
|
||||
} else if npub.is_empty() {
|
||||
"key being made…".to_string()
|
||||
t!("goblin.onboarding.identity.key_being_made").to_string()
|
||||
} else {
|
||||
npub.clone()
|
||||
};
|
||||
@@ -709,9 +727,9 @@ impl OnboardingContent {
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(if connected {
|
||||
"connected over Nym"
|
||||
t!("goblin.onboarding.identity.connected_nym")
|
||||
} else {
|
||||
"connecting over Nym…"
|
||||
t!("goblin.onboarding.identity.connecting_nym")
|
||||
})
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.surface_text_mute),
|
||||
@@ -720,24 +738,15 @@ impl OnboardingContent {
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"A fresh key, made for payments — deliberately not part \
|
||||
of your seed, so you can rotate it anytime to maintain \
|
||||
your privacy, without ever touching your funds. Back it \
|
||||
up in Settings → Identity.",
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
RichText::new(t!("goblin.onboarding.identity.fresh_key_blurb"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"Want a clean slate? Swap in a brand-new key any time — \
|
||||
the new you isn't linked to the old one. Same wallet, \
|
||||
fresh face.",
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
RichText::new(t!("goblin.onboarding.identity.clean_slate_blurb"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
@@ -752,8 +761,13 @@ impl OnboardingContent {
|
||||
self.claim.message = Some(msg.to_string());
|
||||
}
|
||||
ClaimMsg::Registered(nip05) => {
|
||||
self.claim.message =
|
||||
Some(format!("You're @{}", nip05.split('@').next().unwrap_or("")));
|
||||
self.claim.message = Some(
|
||||
t!(
|
||||
"goblin.onboarding.identity.youre",
|
||||
name => nip05.split('@').next().unwrap_or("")
|
||||
)
|
||||
.to_string(),
|
||||
);
|
||||
self.claim.available = Some(true);
|
||||
if let Some(s) = wallet.nostr_service() {
|
||||
{
|
||||
@@ -779,19 +793,15 @@ impl OnboardingContent {
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.label(
|
||||
RichText::new("Pick a username — optional")
|
||||
RichText::new(t!("goblin.onboarding.identity.pick_username"))
|
||||
.font(FontId::new(15.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"Friends pay @you instead of a long key. Public on \
|
||||
goblin.st; payments stay encrypted. Skip it and \
|
||||
you're simply anonymous — claim one any time later.",
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
RichText::new(t!("goblin.onboarding.identity.username_blurb"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
w::field_well(ui, |ui| {
|
||||
@@ -804,7 +814,7 @@ impl OnboardingContent {
|
||||
let before = self.claim.input.clone();
|
||||
TextEdit::new(egui::Id::from("onb_claim"))
|
||||
.focus(false)
|
||||
.hint_text("yourname")
|
||||
.hint_text(t!("goblin.onboarding.identity.username_field_hint"))
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.ui(ui, &mut self.claim.input, cb);
|
||||
@@ -833,21 +843,29 @@ impl OnboardingContent {
|
||||
ui.horizontal(|ui| {
|
||||
View::small_loading_spinner(ui);
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new("Working…").color(t.surface_text_dim));
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.onboarding.identity.working"))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
});
|
||||
ui.ctx().request_repaint();
|
||||
} else {
|
||||
ui.add_enabled_ui(valid && connected, |ui| {
|
||||
if w::big_action_on_card(ui, "Claim username").clicked() {
|
||||
if w::big_action_on_card(
|
||||
ui,
|
||||
&t!("goblin.onboarding.identity.claim_username"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
start_claim_flow(&mut self.claim, &name, &wallet);
|
||||
}
|
||||
});
|
||||
if !connected {
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"Available once the mixnet connects — or skip and claim later.",
|
||||
)
|
||||
RichText::new(t!(
|
||||
"goblin.onboarding.identity.available_when_connected"
|
||||
))
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.surface_text_mute),
|
||||
);
|
||||
@@ -865,11 +883,11 @@ impl OnboardingContent {
|
||||
}
|
||||
|
||||
let main_label = if registered {
|
||||
"Open my wallet"
|
||||
t!("goblin.onboarding.identity.open_wallet")
|
||||
} else {
|
||||
"Skip for now"
|
||||
t!("goblin.onboarding.identity.skip_for_now")
|
||||
};
|
||||
if w::big_action(ui, main_label, false).clicked() {
|
||||
if w::big_action(ui, &main_label, false).clicked() {
|
||||
return Some(wallet);
|
||||
}
|
||||
None
|
||||
|
||||
+150
-111
@@ -288,21 +288,21 @@ impl SendFlow {
|
||||
// Scan-to-pay screen: a Scan | My Code toggle over the camera or your own
|
||||
// payment QR. Replaces the picker until closed.
|
||||
if self.scan_open {
|
||||
if self.back_header(
|
||||
ui,
|
||||
if self.request {
|
||||
"Scan to request"
|
||||
} else {
|
||||
"Scan to pay"
|
||||
},
|
||||
) {
|
||||
let title = if self.request {
|
||||
t!("goblin.send.scan_to_request")
|
||||
} else {
|
||||
t!("goblin.send.scan_to_pay")
|
||||
};
|
||||
if self.back_header(ui, &title) {
|
||||
cb.stop_camera();
|
||||
self.scan = None;
|
||||
self.scan_open = false;
|
||||
return false;
|
||||
}
|
||||
let sel = if self.scan_tab == ScanTab::Scan { 0 } else { 1 };
|
||||
if let Some(i) = w::segmented(ui, &["Scan", "My Code"], sel) {
|
||||
let (tab_scan, tab_my_code) =
|
||||
(t!("goblin.send.tab_scan"), t!("goblin.send.tab_my_code"));
|
||||
if let Some(i) = w::segmented(ui, &[&tab_scan, &tab_my_code], sel) {
|
||||
self.scan_tab = if i == 0 {
|
||||
ScanTab::Scan
|
||||
} else {
|
||||
@@ -331,14 +331,12 @@ impl SendFlow {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.back_header(
|
||||
ui,
|
||||
if self.request {
|
||||
"Request from"
|
||||
} else {
|
||||
"Send to"
|
||||
},
|
||||
) {
|
||||
let title = if self.request {
|
||||
t!("goblin.send.request_from")
|
||||
} else {
|
||||
t!("goblin.send.send_to")
|
||||
};
|
||||
if self.back_header(ui, &title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -365,7 +363,7 @@ impl SendFlow {
|
||||
ui.add_space(8.0);
|
||||
let mut te = TextEdit::new(egui::Id::from("send_search"))
|
||||
.focus(false)
|
||||
.hint_text("@handle, npub, or name")
|
||||
.hint_text(t!("goblin.send.search_hint"))
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.scan_qr();
|
||||
@@ -405,7 +403,7 @@ impl SendFlow {
|
||||
if query.is_empty() {
|
||||
// Empty query → suggested recent peers, as before.
|
||||
ui.label(
|
||||
RichText::new(format!("{} Suggested", USERS))
|
||||
RichText::new(t!("goblin.send.suggested", "icon" => USERS))
|
||||
.font(fonts::kicker())
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -421,7 +419,7 @@ impl SendFlow {
|
||||
if peers.is_empty() {
|
||||
ui.add_space(20.0);
|
||||
ui.label(
|
||||
RichText::new("No contacts yet. Find someone by their @handle.")
|
||||
RichText::new(t!("goblin.send.no_contacts"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
@@ -480,9 +478,16 @@ impl SendFlow {
|
||||
.show(ui, |ui| {
|
||||
for (c, tex) in cands.iter().zip(texs.iter()) {
|
||||
let tag = if c.verified {
|
||||
format!("✓ {}", c.tag)
|
||||
// Localize the human-worded provenance tags; domain / protocol
|
||||
// tags (@goblin.st, nip-05) display verbatim.
|
||||
let label = match c.tag {
|
||||
"contact" => t!("goblin.send.tag_contact"),
|
||||
"on nostr" => t!("goblin.send.tag_on_nostr"),
|
||||
other => std::borrow::Cow::Borrowed(other),
|
||||
};
|
||||
format!("✓ {}", label)
|
||||
} else {
|
||||
"no profile".to_string()
|
||||
t!("goblin.send.no_profile").to_string()
|
||||
};
|
||||
if w::activity_row(
|
||||
ui,
|
||||
@@ -506,7 +511,7 @@ impl SendFlow {
|
||||
View::small_loading_spinner(ui);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("Searching nostr…")
|
||||
RichText::new(t!("goblin.send.searching_nostr"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
@@ -550,19 +555,15 @@ impl SendFlow {
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.label(
|
||||
RichText::new("Pay an unverified key?")
|
||||
RichText::new(t!("goblin.send.unverified_title"))
|
||||
.font(FontId::new(15.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"No nostr profile is published for this key — it may be \
|
||||
brand new, anonymous, or mistyped. Double-check it's the \
|
||||
right one before sending.",
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
RichText::new(t!("goblin.send.unverified_body"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
@@ -579,7 +580,7 @@ impl SendFlow {
|
||||
Vec2::new(half, 44.0),
|
||||
)),
|
||||
|ui| {
|
||||
if w::big_action_on_card(ui, "Keep looking").clicked() {
|
||||
if w::big_action_on_card(ui, &t!("goblin.send.keep_looking")).clicked() {
|
||||
self.confirm_unverified = None;
|
||||
}
|
||||
},
|
||||
@@ -591,7 +592,7 @@ impl SendFlow {
|
||||
Vec2::new(half, 44.0),
|
||||
)),
|
||||
|ui| {
|
||||
if w::big_action(ui, "Pay anyway", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.pay_anyway"), false).clicked() {
|
||||
let mut c = cand.clone();
|
||||
c.verified = true;
|
||||
self.confirm_unverified = None;
|
||||
@@ -605,7 +606,6 @@ impl SendFlow {
|
||||
|
||||
/// Camera feed scanning for a recipient QR (npub / nostr: URI / @handle).
|
||||
fn scan_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
const NO_RECIPIENT: &str = "That QR isn't a goblin recipient — expected an npub or @handle";
|
||||
let t = theme::tokens();
|
||||
let result = self.scan.as_mut().and_then(|cam| {
|
||||
let res = cam.qr_scan_result();
|
||||
@@ -637,14 +637,14 @@ impl SendFlow {
|
||||
self.net_candidate = None;
|
||||
let _ = wallet;
|
||||
}
|
||||
_ => self.error = Some(NO_RECIPIENT.to_string()),
|
||||
_ => self.error = Some(t!("goblin.send.scan_not_recipient").to_string()),
|
||||
}
|
||||
return;
|
||||
}
|
||||
ui.add_space(14.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("Position a goblin code in view to activate")
|
||||
RichText::new(t!("goblin.send.scan_prompt"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
@@ -676,7 +676,7 @@ impl SendFlow {
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
ui.label(
|
||||
RichText::new("Scan to pay me")
|
||||
RichText::new(t!("goblin.send.scan_to_pay_me"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
@@ -688,7 +688,7 @@ impl SendFlow {
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
if w::big_action(ui, &format!("{} Share", SHARE), false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.share_btn", "icon" => SHARE), false).clicked() {
|
||||
// Share the full nostr identity (npub + relay hints), with the bare
|
||||
// npub as a fallback line, via the platform's native share sheet.
|
||||
let link = if nprofile.is_empty() {
|
||||
@@ -696,7 +696,13 @@ impl SendFlow {
|
||||
} else {
|
||||
format!("nostr:{}", nprofile)
|
||||
};
|
||||
let msg = format!("Pay me on Goblin — {}\n{}\nnpub: {}", handle, link, npub);
|
||||
let msg = t!(
|
||||
"goblin.send.share_message",
|
||||
"handle" => handle,
|
||||
"link" => link,
|
||||
"npub" => npub
|
||||
)
|
||||
.to_string();
|
||||
cb.share_text(msg);
|
||||
}
|
||||
}
|
||||
@@ -716,7 +722,8 @@ impl SendFlow {
|
||||
}
|
||||
LookupResult::NotFound(label) => {
|
||||
self.net_candidate = None;
|
||||
self.error = Some(format!("No one found for {label}"));
|
||||
self.error =
|
||||
Some(t!("goblin.send.none_found", "label" => label).to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -841,7 +848,7 @@ impl SendFlow {
|
||||
*slot.lock().unwrap() = Some(res);
|
||||
});
|
||||
} else {
|
||||
self.error = Some("Enter an @handle, npub, or name".to_string());
|
||||
self.error = Some(t!("goblin.send.enter_recipient").to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,7 +860,7 @@ impl SendFlow {
|
||||
cb: &dyn PlatformCallbacks,
|
||||
) -> bool {
|
||||
let t = theme::tokens();
|
||||
if self.back_header(ui, "Amount") {
|
||||
if self.back_header(ui, &t!("goblin.send.amount_title")) {
|
||||
self.stage = Stage::Recipient;
|
||||
return false;
|
||||
}
|
||||
@@ -870,7 +877,7 @@ impl SendFlow {
|
||||
.unwrap_or(false);
|
||||
|
||||
// Recipient chip, centered per the design.
|
||||
let name_label = format!("To {}", recipient.name);
|
||||
let name_label = t!("goblin.send.to_name", "name" => recipient.name).to_string();
|
||||
let name_galley = ui.painter().layout_no_wrap(
|
||||
name_label.clone(),
|
||||
FontId::new(14.0, fonts::semibold()),
|
||||
@@ -924,7 +931,7 @@ impl SendFlow {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("You don't have enough grin")
|
||||
RichText::new(t!("goblin.send.not_enough"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
@@ -936,7 +943,12 @@ impl SendFlow {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space((ui.available_width() - 220.0).max(0.0) / 2.0);
|
||||
for v in ["1", "10", "100", "Max"] {
|
||||
if w::chip_outline(ui, v).clicked() {
|
||||
let label = if v == "Max" {
|
||||
t!("goblin.send.max")
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(v)
|
||||
};
|
||||
if w::chip_outline(ui, &label).clicked() {
|
||||
if v == "Max" {
|
||||
let max = wallet
|
||||
.get_data()
|
||||
@@ -957,7 +969,7 @@ impl SendFlow {
|
||||
w::card(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::new("Note")
|
||||
RichText::new(t!("goblin.send.note_label"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
@@ -965,7 +977,7 @@ impl SendFlow {
|
||||
let note_id = egui::Id::from("send_note");
|
||||
TextEdit::new(note_id)
|
||||
.focus(false)
|
||||
.hint_text("Add a note…")
|
||||
.hint_text(t!("goblin.send.note_hint"))
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.ui(ui, &mut self.note, cb);
|
||||
@@ -988,7 +1000,7 @@ impl SendFlow {
|
||||
.map(|a| a > 0)
|
||||
.unwrap_or(false);
|
||||
ui.add_enabled_ui(valid, |ui| {
|
||||
if w::big_action(ui, "Review", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.review_btn"), false).clicked() {
|
||||
if over {
|
||||
cb.vibrate_error();
|
||||
} else {
|
||||
@@ -1006,14 +1018,12 @@ impl SendFlow {
|
||||
avatars: &mut AvatarTextures,
|
||||
) -> bool {
|
||||
let t = theme::tokens();
|
||||
if self.back_header(
|
||||
ui,
|
||||
if self.request {
|
||||
"Confirm request"
|
||||
} else {
|
||||
"Review"
|
||||
},
|
||||
) {
|
||||
let title = if self.request {
|
||||
t!("goblin.send.confirm_request")
|
||||
} else {
|
||||
t!("goblin.send.review_title")
|
||||
};
|
||||
if self.back_header(ui, &title) {
|
||||
// Requests fix the amount on the Pay tab, so back returns to the
|
||||
// recipient picker rather than the send-style amount step.
|
||||
self.stage = if self.request {
|
||||
@@ -1045,9 +1055,9 @@ impl SendFlow {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.add_space(8.0);
|
||||
let label = if self.request {
|
||||
format!("Requesting from {}", recipient.name)
|
||||
t!("goblin.send.requesting_from", "name" => recipient.name).to_string()
|
||||
} else {
|
||||
format!("You're sending {}", recipient.name)
|
||||
t!("goblin.send.youre_sending", "name" => recipient.name).to_string()
|
||||
};
|
||||
// Centered avatar + caption. A long counterparty (a bare npub) wraps
|
||||
// and stays centered instead of overflowing the card.
|
||||
@@ -1073,34 +1083,59 @@ impl SendFlow {
|
||||
});
|
||||
ui.add_space(16.0);
|
||||
|
||||
w::info_row(
|
||||
ui,
|
||||
if self.request { "From" } else { "To" },
|
||||
&recipient.name,
|
||||
);
|
||||
let from_to = if self.request {
|
||||
t!("goblin.send.row_from")
|
||||
} else {
|
||||
t!("goblin.send.row_to")
|
||||
};
|
||||
w::info_row(ui, &from_to, &recipient.name);
|
||||
if !self.note.trim().is_empty() {
|
||||
w::info_row(ui, "Note", &format!("\u{201C}{}\u{201D}", self.note.trim()));
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_note"),
|
||||
&format!("\u{201C}{}\u{201D}", self.note.trim()),
|
||||
);
|
||||
}
|
||||
if self.request {
|
||||
w::info_row(ui, "They pay", "Only if they approve");
|
||||
w::info_row(ui, "Delivery", "NIP-44 encrypted, over Nym");
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_they_pay"),
|
||||
&t!("goblin.send.row_they_pay_val"),
|
||||
);
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_delivery"),
|
||||
&t!("goblin.send.row_delivery_val"),
|
||||
);
|
||||
} else {
|
||||
w::info_row(ui, "Network fee", "Deducted from your balance");
|
||||
w::info_row(ui, "Privacy", "Mimblewimble + Nym");
|
||||
w::info_row(ui, "Delivery", "NIP-44 encrypted, over Nym");
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_network_fee"),
|
||||
&t!("goblin.send.row_network_fee_val"),
|
||||
);
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_privacy"),
|
||||
&t!("goblin.send.row_privacy_val"),
|
||||
);
|
||||
w::info_row(
|
||||
ui,
|
||||
&t!("goblin.send.row_delivery"),
|
||||
&t!("goblin.send.row_delivery_val"),
|
||||
);
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Requests are not a spend: one tap sends the ask, no hold-to-confirm.
|
||||
if self.request {
|
||||
if w::big_action(ui, "Send request", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.send_request_btn"), false).clicked() {
|
||||
self.dispatch(wallet);
|
||||
self.stage = Stage::Sending;
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("They'll get a request to approve")
|
||||
RichText::new(t!("goblin.send.request_approve_hint"))
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
@@ -1111,7 +1146,7 @@ impl SendFlow {
|
||||
if over {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("You don't have enough grin")
|
||||
RichText::new(t!("goblin.send.not_enough"))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
@@ -1121,7 +1156,7 @@ impl SendFlow {
|
||||
// Greyed out while over balance; the `&& !over` also refuses the send in
|
||||
// case the hold widget ignores the disabled state.
|
||||
ui.add_enabled_ui(!over, |ui| {
|
||||
if self.hold.ui(ui, "Hold to send") && !over {
|
||||
if self.hold.ui(ui, &t!("goblin.send.hold_to_send")) && !over {
|
||||
self.dispatch(wallet);
|
||||
self.stage = Stage::Sending;
|
||||
}
|
||||
@@ -1130,9 +1165,9 @@ impl SendFlow {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new(if over {
|
||||
"Go back and lower the amount"
|
||||
t!("goblin.send.lower_amount")
|
||||
} else {
|
||||
"Press and hold to confirm"
|
||||
t!("goblin.send.hold_confirm_hint")
|
||||
})
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
@@ -1183,9 +1218,9 @@ impl SendFlow {
|
||||
ui.add_space(16.0);
|
||||
ui.label(
|
||||
RichText::new(if self.request {
|
||||
"Requesting…"
|
||||
t!("goblin.send.requesting")
|
||||
} else {
|
||||
"Sending…"
|
||||
t!("goblin.send.sending")
|
||||
})
|
||||
.font(FontId::new(18.0, fonts::semibold()))
|
||||
.color(t.text),
|
||||
@@ -1204,11 +1239,8 @@ impl SendFlow {
|
||||
.recipient
|
||||
.as_ref()
|
||||
.map(|r| r.name.clone())
|
||||
.unwrap_or_else(|| "They".to_string());
|
||||
self.error = Some(format!(
|
||||
"{} isn't accepting requests. Ask them to send you grin instead.",
|
||||
who
|
||||
));
|
||||
.unwrap_or_else(|| t!("goblin.send.they").to_string());
|
||||
self.error = Some(t!("goblin.send.request_blocked", "who" => who).to_string());
|
||||
self.stage = Stage::Failed;
|
||||
}
|
||||
crate::nostr::send_phase::FAILED => self.stage = Stage::Failed,
|
||||
@@ -1230,9 +1262,9 @@ impl SendFlow {
|
||||
ui.add_space(16.0);
|
||||
ui.label(
|
||||
RichText::new(if self.request {
|
||||
"Couldn't request"
|
||||
t!("goblin.send.failed_request_title")
|
||||
} else {
|
||||
"Couldn't send"
|
||||
t!("goblin.send.failed_send_title")
|
||||
})
|
||||
.font(FontId::new(22.0, fonts::bold()))
|
||||
.color(t.text),
|
||||
@@ -1241,10 +1273,9 @@ impl SendFlow {
|
||||
ui.label(
|
||||
RichText::new(self.error.clone().unwrap_or_else(|| {
|
||||
if self.request {
|
||||
"We couldn't deliver the request. Ask them to send you grin instead."
|
||||
.to_string()
|
||||
t!("goblin.send.failed_request_body").to_string()
|
||||
} else {
|
||||
"The payment wasn't delivered. Your grin is safe — try again.".to_string()
|
||||
t!("goblin.send.failed_send_body").to_string()
|
||||
}
|
||||
}))
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
@@ -1252,12 +1283,12 @@ impl SendFlow {
|
||||
);
|
||||
});
|
||||
ui.add_space(24.0);
|
||||
if w::big_action(ui, "Try again", false).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.try_again_btn"), false).clicked() {
|
||||
self.dispatch(wallet);
|
||||
self.stage = Stage::Sending;
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
if w::big_action(ui, "Close", true).clicked() {
|
||||
if w::big_action(ui, &t!("goblin.send.close_btn"), true).clicked() {
|
||||
done = true;
|
||||
}
|
||||
done
|
||||
@@ -1281,32 +1312,40 @@ impl SendFlow {
|
||||
);
|
||||
ui.add_space(24.0);
|
||||
ui.label(
|
||||
RichText::new(if self.request { "Requested" } else { "Sent" })
|
||||
RichText::new(if self.request { t!("goblin.send.success.requested") } else { t!("goblin.send.success.sent") })
|
||||
.font(FontId::new(34.0, fonts::bold()))
|
||||
.color(t.accent_ink),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
let total_w = ui.available_width();
|
||||
ui.add_space(total_w / 2.0 - 60.0);
|
||||
ui.label(
|
||||
RichText::new(&self.amount)
|
||||
.font(FontId::new(40.0, fonts::mono_semibold()))
|
||||
.color(t.accent_ink),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(w::TSU)
|
||||
.font(FontId::new(20.0, fonts::medium()))
|
||||
.color(t.accent_ink),
|
||||
);
|
||||
});
|
||||
// Amount + ツ as one layout job so vertical_centered centers it exactly,
|
||||
// independent of the number's width (a fixed offset drifts off-center).
|
||||
let mut job = egui::text::LayoutJob::default();
|
||||
job.append(
|
||||
&self.amount,
|
||||
0.0,
|
||||
egui::text::TextFormat {
|
||||
font_id: FontId::new(40.0, fonts::mono_semibold()),
|
||||
color: t.accent_ink,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
job.append(
|
||||
w::TSU,
|
||||
0.0,
|
||||
egui::text::TextFormat {
|
||||
font_id: FontId::new(20.0, fonts::medium()),
|
||||
color: t.accent_ink,
|
||||
valign: Align::BOTTOM,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
ui.label(job);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(format!(
|
||||
"{} {} · just now",
|
||||
if self.request { "from" } else { "to" },
|
||||
recipient.name
|
||||
RichText::new(t!(
|
||||
"goblin.send.success.subtitle",
|
||||
"dir" => if self.request { t!("goblin.send.success.from") } else { t!("goblin.send.success.to") },
|
||||
"who" => recipient.name
|
||||
))
|
||||
.font(FontId::new(15.0, fonts::regular()))
|
||||
.color(t.accent_ink.gamma_multiply(0.7)),
|
||||
@@ -1330,7 +1369,7 @@ impl SendFlow {
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"Done",
|
||||
t!("goblin.send.success.done_btn"),
|
||||
FontId::new(17.0, fonts::semibold()),
|
||||
t.accent,
|
||||
);
|
||||
@@ -1352,7 +1391,7 @@ impl SendFlow {
|
||||
ui.painter().text(
|
||||
r2.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"Receipt",
|
||||
t!("goblin.send.success.receipt_btn"),
|
||||
FontId::new(17.0, fonts::semibold()),
|
||||
t.accent_ink,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user