Compare commits
4
Commits
097b2ea014
...
8961cbdfc9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8961cbdfc9 | ||
|
|
794150b11b | ||
|
|
2e74a3ca69 | ||
|
|
e94d88e723 |
@@ -1226,6 +1226,7 @@ pill-rounding = { size = 12.0 }
|
||||
padding-x = { size = 12.0 }
|
||||
glass-fill-alpha = { size = 18.0 }
|
||||
accent-fill-alpha = { size = 0.22 } # severity-tint fill opacity (fraction of the accent alpha)
|
||||
badge-fill-alpha = { size = 0.35 } # "+N more" badge fill opacity (fraction of the accent alpha)
|
||||
glass-border-alpha = { size = 35.0 }
|
||||
progress-bar-height = { size = 2.0 }
|
||||
duration-info = { size = 1.0 }
|
||||
|
||||
+1
-1
@@ -3317,7 +3317,7 @@ void App::renderStatusBar()
|
||||
ImVec2(bellMax.x - r, bellMin.y + r), r, dotCol);
|
||||
}
|
||||
|
||||
if (clicked) {
|
||||
if (clicked || notes.consumeHistoryRequest()) { // bell click, or a click on the toast itself
|
||||
alerts_seen_total_ = notes.totalPushed(); // mark everything currently shown as seen
|
||||
ImGui::OpenPopup("##AlertHistoryPopup");
|
||||
}
|
||||
|
||||
@@ -271,6 +271,7 @@ bool Settings::load(const std::string& path)
|
||||
}
|
||||
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
||||
loadScalar(j, "chat_intro_dismissed", chat_intro_dismissed_);
|
||||
loadScalar(j, "lock_failed_attempts", lock_failed_attempts_);
|
||||
loadScalar(j, "lockout_until_epoch", lockout_until_epoch_);
|
||||
loadScalar(j, "lock_state_wallet", lock_state_wallet_);
|
||||
@@ -599,6 +600,7 @@ bool Settings::save(const std::string& path)
|
||||
}
|
||||
j["wizard_completed"] = wizard_completed_;
|
||||
j["seed_backup_reminded"] = seed_backup_reminded_;
|
||||
j["chat_intro_dismissed"] = chat_intro_dismissed_;
|
||||
j["lock_failed_attempts"] = lock_failed_attempts_;
|
||||
j["lockout_until_epoch"] = lockout_until_epoch_;
|
||||
j["lock_state_wallet"] = lock_state_wallet_;
|
||||
|
||||
@@ -392,6 +392,8 @@ public:
|
||||
// Whether the one-time "back up your seed phrase" reminder has already been shown.
|
||||
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
||||
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
||||
bool getChatIntroDismissed() const { return chat_intro_dismissed_; }
|
||||
void setChatIntroDismissed(bool v) { chat_intro_dismissed_ = v; durable_next_save_ = true; }
|
||||
|
||||
// Unlock lockout state, persisted across restarts so the failed-attempt curve can't be reset by
|
||||
// killing the app. Keyed by the wallet file it applies to; the PIN/passphrase is never stored.
|
||||
@@ -745,6 +747,7 @@ private:
|
||||
std::map<std::string, AddressMeta> address_meta_;
|
||||
bool wizard_completed_ = false;
|
||||
bool seed_backup_reminded_ = false;
|
||||
bool chat_intro_dismissed_ = false; // one-time chat "how it works" intro card
|
||||
int lock_failed_attempts_ = 0; // persisted unlock-failure count (lockout curve)
|
||||
long long lockout_until_epoch_ = 0; // unix time the current lockout expires (0 = none)
|
||||
std::string lock_state_wallet_; // wallet file the above two apply to
|
||||
|
||||
@@ -100,6 +100,15 @@ void Notifications::render()
|
||||
float iconGap = 4.0f * dp;
|
||||
float msgW = textFont ? textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, notif.message.c_str()).x : 100.0f;
|
||||
float pillWidth = pillPadX + iconW + iconGap + msgW + pillPadX;
|
||||
// "+N more" badge when several live toasts are queued (render shows only the newest).
|
||||
const size_t moreCount = hiddenLiveCount();
|
||||
std::string badge = moreCount ? ("+" + std::to_string(moreCount)) : std::string();
|
||||
float badgeW = 0.0f, badgeGap = 0.0f;
|
||||
if (!badge.empty() && textFont) {
|
||||
badgeW = textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, badge.c_str()).x + 10.0f * dp;
|
||||
badgeGap = 6.0f * dp;
|
||||
pillWidth += badgeW + badgeGap;
|
||||
}
|
||||
// Clamp to reasonable bounds
|
||||
float maxPillW = viewport->WorkSize.x * 0.5f;
|
||||
if (pillWidth > maxPillW) pillWidth = maxPillW;
|
||||
@@ -163,7 +172,7 @@ void Notifications::render()
|
||||
bool clipped = false;
|
||||
if (textFont) {
|
||||
ImU32 textCol = ImGui::ColorConvertFloat4ToU32(text_color);
|
||||
const float availTextW = (pMax.x - pillPadX) - cursorX;
|
||||
const float availTextW = (pMax.x - pillPadX) - cursorX - (badge.empty() ? 0.0f : (badgeW + badgeGap));
|
||||
std::string shown = notif.message;
|
||||
if (availTextW > 0.0f &&
|
||||
textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, shown.c_str()).x > availTextW) {
|
||||
@@ -183,8 +192,23 @@ void Notifications::render()
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
// Hover: pause the auto-dismiss (handled next frame) and reveal the full message if it was clipped.
|
||||
// "+N more" badge, right-aligned inside the pill.
|
||||
if (!badge.empty() && textFont) {
|
||||
float badgeX = pMax.x - pillPadX - badgeW;
|
||||
ImVec4 bf = accent_color; bf.w *= nde("badge-fill-alpha", 0.35f);
|
||||
dl->AddRectFilled(ImVec2(badgeX, pillY + 3.0f * dp), ImVec2(badgeX + badgeW, pMax.y - 3.0f * dp),
|
||||
ImGui::ColorConvertFloat4ToU32(bf), (pillHeight - 6.0f * dp) * 0.5f);
|
||||
dl->AddText(textFont, textFont->LegacySize, ImVec2(badgeX + 5.0f * dp, contentY),
|
||||
ImGui::ColorConvertFloat4ToU32(text_color), badge.c_str());
|
||||
}
|
||||
|
||||
// Hover: pause the auto-dismiss (next frame), reveal the full message if clipped, and allow a click to
|
||||
// open the notification history (the status bar consumes the request).
|
||||
toast_hovered_ = ImGui::IsMouseHoveringRect(pMin, pMax);
|
||||
if (toast_hovered_) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) requestHistory();
|
||||
}
|
||||
if (toast_hovered_ && clipped)
|
||||
ImGui::SetTooltip("%s", notif.message.c_str());
|
||||
}
|
||||
|
||||
@@ -169,8 +169,20 @@ public:
|
||||
|
||||
void clear() {
|
||||
notifications_.clear();
|
||||
history_requested_ = false;
|
||||
}
|
||||
|
||||
/// Number of currently-live toasts beyond the one the pill shows (for a "+N more" badge).
|
||||
size_t hiddenLiveCount() const {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
size_t live = 0;
|
||||
for (const auto& n : notifications_) if (!n.isExpiredAt(now)) ++live;
|
||||
return live > 1 ? live - 1 : 0;
|
||||
}
|
||||
/// render() sets this when the toast is clicked; the status bar consumes it to open the history panel.
|
||||
void requestHistory() { history_requested_ = true; }
|
||||
bool consumeHistoryRequest() { bool r = history_requested_; history_requested_ = false; return r; }
|
||||
|
||||
void setMaxNotifications(size_t max) {
|
||||
max_notifications_ = max;
|
||||
}
|
||||
@@ -191,6 +203,7 @@ private:
|
||||
Notifications& operator=(const Notifications&) = delete;
|
||||
|
||||
std::deque<Notification> notifications_;
|
||||
bool history_requested_ = false; // set on toast click, consumed by the status bar to open history
|
||||
size_t max_notifications_ = 5;
|
||||
bool toast_hovered_ = false; // pointer was over the toast last frame → pause its auto-dismiss
|
||||
std::chrono::steady_clock::time_point last_drawn_{};
|
||||
|
||||
+40
-11
@@ -591,6 +591,17 @@ void centeredHint(const char* text) {
|
||||
}
|
||||
|
||||
// Centered empty state: big muted icon + title + optional wrapped hint (V4).
|
||||
// Copy the user's own chat address to the clipboard (shared by the sidebar button and the intro card).
|
||||
void copyMyChatAddress(App* app) {
|
||||
std::string myAddr = app->chatMyAddress();
|
||||
if (myAddr.empty()) {
|
||||
Notifications::instance().warning(TR("chat_no_my_address"));
|
||||
} else {
|
||||
ImGui::SetClipboardText(myAddr.c_str());
|
||||
Notifications::instance().info(TR("address_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
void centeredEmptyState(const char* icon, const char* title, const char* hint) {
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const ImVec2 origin = ImGui::GetCursorPos();
|
||||
@@ -956,15 +967,7 @@ void RenderChatTab(App* app)
|
||||
{
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
// Text-only: SmallButton uses the body font, which has no Material icon glyph (would show "?").
|
||||
if (ImGui::SmallButton(TR("chat_copy_my_address"))) {
|
||||
std::string myAddr = app->chatMyAddress();
|
||||
if (myAddr.empty()) {
|
||||
Notifications::instance().warning(TR("chat_no_my_address"));
|
||||
} else {
|
||||
ImGui::SetClipboardText(myAddr.c_str());
|
||||
Notifications::instance().info(TR("address_copied"));
|
||||
}
|
||||
}
|
||||
if (ImGui::SmallButton(TR("chat_copy_my_address"))) copyMyChatAddress(app);
|
||||
ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("chat_my_address_tip"));
|
||||
}
|
||||
@@ -1875,9 +1878,35 @@ void RenderChatTab(App* app)
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2); // ##ChatMessages WindowPadding + zeroed vertical ItemSpacing
|
||||
// Composer is rendered OUTSIDE this box, directly below it (see after EndChild).
|
||||
} else if (convs.empty() && app->settings() && !app->settings()->getChatIntroDismissed()) {
|
||||
// First-run intro: explain how chat works + let the user copy their address, instead of a bare
|
||||
// "no conversations yet" line. Dismissal is persisted.
|
||||
const float idp = Layout::dpiScale();
|
||||
const float availX = ImGui::GetContentRegionAvail().x;
|
||||
const float cardW = std::min(availX - 40.0f * idp, 460.0f * idp);
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (availX - cardW) * 0.5f));
|
||||
ImGui::BeginGroup();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + cardW);
|
||||
material::Type().textColored(material::TypeStyle::Subtitle1, material::OnSurface(), TR("chat_intro_title"));
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::TextWrapped("%s", TR("chat_intro_body"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", TR("chat_intro_fee"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
if (material::TactileButton(TR("chat_copy_my_address"), ImVec2(cardW, 0))) copyMyChatAddress(app);
|
||||
ImGui::Spacing();
|
||||
if (material::TactileButton(TR("chat_intro_dismiss"), ImVec2(cardW, 0))) {
|
||||
app->settings()->setChatIntroDismissed(true);
|
||||
app->settings()->save();
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
} else if (convs.empty()) {
|
||||
centeredEmptyState(ICON_MD_FORUM, TR("chat_empty_title"), TR("chat_empty_start"));
|
||||
} else {
|
||||
if (convs.empty()) centeredEmptyState(ICON_MD_FORUM, TR("chat_empty_title"), TR("chat_empty_start"));
|
||||
else centeredEmptyState(ICON_MD_CHAT_BUBBLE_OUTLINE, TR("chat_select_hint"), nullptr);
|
||||
centeredEmptyState(ICON_MD_CHAT_BUBBLE_OUTLINE, TR("chat_select_hint"), nullptr);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -1552,6 +1552,12 @@ bool IsDestructiveConsoleCommand(const std::string& firstLower, bool fullNode)
|
||||
if (n == firstLower) return cat.commands[i].destructive;
|
||||
}
|
||||
}
|
||||
// Secret-revealing full-node commands that aren't in the reference catalog still deserve the gate
|
||||
// (e.g. z_exportmnemonic dumps the seed phrase; the lite "seed" equivalent is already flagged).
|
||||
if (fullNode) {
|
||||
static const char* kSecretReveal[] = { "z_exportmnemonic", "z_exportviewingkey" };
|
||||
for (const char* s : kSecretReveal) if (firstLower == s) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
@@ -1882,6 +1888,9 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
|
||||
ImGui::SameLine();
|
||||
if (material::TactileButton(TR("console_ref_run"), ImVec2(cbw, 0))) {
|
||||
pending_submit_ = built;
|
||||
// The reference dialog already confirmed this run, so pre-arm the destructive gate to an exact
|
||||
// match — otherwise the replayed submit re-arms it and silently does nothing until retyped.
|
||||
destructive_confirm_cmd_ = built;
|
||||
command_search_[0] = '\0';
|
||||
run_confirm_cmd_ = nullptr;
|
||||
show_commands_popup_ = false;
|
||||
|
||||
@@ -269,7 +269,7 @@ static void fetchBlockDetail(App* app, int height) {
|
||||
auto hashResult = rpc->call("getblockhash", {height});
|
||||
std::string hash = hashResult.get<std::string>();
|
||||
result = rpc->call("getblock", {hash});
|
||||
} catch (const std::exception& e) { error = e.what(); }
|
||||
} catch (const std::exception& e) { error = util::friendlyRpcError(e.what()); }
|
||||
return [result, error]() {
|
||||
applyBlockDetailResult(result, error);
|
||||
};
|
||||
@@ -383,19 +383,27 @@ static void navigateToHash(App* app, const std::string& hash) {
|
||||
worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb {
|
||||
json blockResult, txResult;
|
||||
bool gotBlock = false, gotTx = false;
|
||||
bool rpcFailed = false; std::string rpcErr;
|
||||
// Distinguish a genuine "not found" (RPC -5/-8) from a transport/timeout failure, so a flaky
|
||||
// connection isn't reported as "no block or transaction found".
|
||||
auto noteErr = [&](const std::exception& e) {
|
||||
const auto* re = dynamic_cast<const rpc::RpcError*>(&e);
|
||||
if (re && (re->code == -5 || re->code == -8)) return; // not-found: a real negative answer
|
||||
if (!rpcFailed) { rpcFailed = true; rpcErr = e.what(); }
|
||||
};
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
||||
blockResult = rpc->call("getblock", {hash});
|
||||
gotBlock = true;
|
||||
} catch (...) {}
|
||||
} catch (const std::exception& e) { noteErr(e); }
|
||||
if (!gotBlock) {
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
||||
txResult = rpc->call("getrawtransaction", {hash, 1});
|
||||
gotTx = true;
|
||||
} catch (...) {}
|
||||
} catch (const std::exception& e) { noteErr(e); }
|
||||
}
|
||||
return [blockResult, txResult, gotBlock, gotTx]() {
|
||||
return [blockResult, txResult, gotBlock, gotTx, rpcFailed, rpcErr]() {
|
||||
s_search_loading = false;
|
||||
if (gotBlock) {
|
||||
int h = blockResult.value("height", -1);
|
||||
@@ -407,6 +415,8 @@ static void navigateToHash(App* app, const std::string& hash) {
|
||||
s_tx_loading = false;
|
||||
s_expanded_tx_idx = 0;
|
||||
s_show_detail_modal = false;
|
||||
} else if (rpcFailed) {
|
||||
s_search_error = std::string(TR("explorer_lookup_failed")) + ": " + util::friendlyRpcError(rpcErr);
|
||||
} else {
|
||||
s_search_error = TR("explorer_hash_not_found");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
#include <string>
|
||||
#include <ctime>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
@@ -28,6 +30,31 @@ static char s_filename[256] = "";
|
||||
static std::string s_saved_path; // full path on success (shown as a copy field); empty otherwise
|
||||
static std::string s_error; // error message on failure; empty otherwise
|
||||
|
||||
// Make a user-typed filename safe to write into the config dir: basename only, illegal chars → '_',
|
||||
// no leading dots / trailing dots-or-spaces, length-capped, always ending in .csv.
|
||||
static std::string sanitizeExportFilename(std::string name) {
|
||||
const size_t slash = name.find_last_of("/\\");
|
||||
if (slash != std::string::npos) name = name.substr(slash + 1);
|
||||
auto notspace = [](unsigned char c) { return !std::isspace(c); };
|
||||
name.erase(name.begin(), std::find_if(name.begin(), name.end(), notspace));
|
||||
name.erase(std::find_if(name.rbegin(), name.rend(), notspace).base(), name.end());
|
||||
for (char& c : name) {
|
||||
const unsigned char u = static_cast<unsigned char>(c);
|
||||
if (u < 0x20 || c == '<' || c == '>' || c == ':' || c == '"' || c == '|' || c == '?' || c == '*')
|
||||
c = '_';
|
||||
}
|
||||
const size_t firstGood = name.find_first_not_of('.');
|
||||
if (firstGood == std::string::npos) name.clear(); else name.erase(0, firstGood);
|
||||
while (!name.empty() && (name.back() == '.' || name.back() == ' ')) name.pop_back();
|
||||
if (name.size() > 120) name.resize(120);
|
||||
std::string lower = name;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lower.size() < 4 || lower.compare(lower.size() - 4, 4, ".csv") != 0) name += ".csv";
|
||||
if (name.empty() || name == ".csv") name = "dragonx_transactions.csv";
|
||||
return name;
|
||||
}
|
||||
|
||||
void ExportTransactionsDialog::show()
|
||||
{
|
||||
s_open = true;
|
||||
@@ -94,6 +121,8 @@ void ExportTransactionsDialog::render(App* app)
|
||||
ImGui::SameLine(0, 4.0f * Layout::dpiScale());
|
||||
ImGui::TextColored(m::SuccessVec4(), "%s", TR("export_tx_success"));
|
||||
widgets::AddressCopyField("##exportedpath", s_saved_path);
|
||||
if (m::TactileButton(TR("export_tx_open_folder"), ImVec2(0, 0)))
|
||||
util::Platform::openFolder(util::Platform::getConfigDir());
|
||||
} else if (!s_error.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
@@ -113,7 +142,11 @@ void ExportTransactionsDialog::render(App* app)
|
||||
Notifications::instance().warning(TR("export_tx_none"));
|
||||
} else {
|
||||
std::string configDir = util::Platform::getConfigDir();
|
||||
std::string filepath = configDir + "/" + s_filename;
|
||||
// Sanitize in-buffer so the user sees exactly what was written (and we can't be pointed outside
|
||||
// the config dir by a pasted path).
|
||||
std::string safeName = sanitizeExportFilename(s_filename);
|
||||
snprintf(s_filename, sizeof(s_filename), "%s", safeName.c_str());
|
||||
std::string filepath = configDir + "/" + safeName;
|
||||
// Build in memory, then write atomically (temp + fsync + rename): a disk-full /
|
||||
// removable-media failure is reported instead of leaving a truncated CSV behind a success toast.
|
||||
const std::string csv = data::formatTransactionsCsv(state.transactions);
|
||||
|
||||
@@ -121,7 +121,7 @@ void KeyExportDialog::render(App* app)
|
||||
ImGui::Text("%s", key_label);
|
||||
|
||||
if (s_fetching) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("key_export_fetching"));
|
||||
ImGui::TextColored(material::WarningVec4(), "%s%s", TR("key_export_fetching_base"), material::LoadingDots());
|
||||
} else if (!s_error.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str());
|
||||
// Not a dead-end: clearing the error falls back through to the Reveal button next frame.
|
||||
@@ -133,6 +133,16 @@ void KeyExportDialog::render(App* app)
|
||||
// Guard against an empty/blatantly-malformed address before dispatching a doomed reveal.
|
||||
// (Also avoids reading s_address[0] on an empty string in the Reveal branch below.)
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("invalid_address"));
|
||||
} else if (s_key.empty() && app->getWalletState().isLocked()) {
|
||||
// Reveal would just fail with a locked-wallet RPC error — say so and offer to unlock.
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::Warning());
|
||||
ImGui::TextWrapped("%s", TR("key_export_unlock_first"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
if (material::TactileButton(TR("lite_unlock"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
||||
if (app->liteWallet()) app->requestLiteUnlock();
|
||||
else s_open = false; // full node: closing reveals the lock screen already shown while locked
|
||||
}
|
||||
} else if (s_key.empty()) {
|
||||
// Show button to fetch key
|
||||
if (material::TactileButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
||||
@@ -173,7 +183,7 @@ void KeyExportDialog::render(App* app)
|
||||
s_key = found;
|
||||
s_show_key = wantViewing; // viewing keys are less sensitive
|
||||
} else {
|
||||
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
|
||||
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : util::friendlyRpcError(r.error);
|
||||
}
|
||||
wallet::secureWipeLiteSecret(found);
|
||||
s_fetching = false;
|
||||
|
||||
@@ -268,6 +268,10 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_retry"] = "Retry";
|
||||
strings_["chat_jump_latest"] = "Latest";
|
||||
strings_["chat_empty_title"] = "No conversations yet";
|
||||
strings_["chat_intro_title"] = "How chat works";
|
||||
strings_["chat_intro_body"] = "Messages are end-to-end encrypted and sent as shielded transactions, so only you and your contact can read them. Your chat identity is tied to your wallet seed, so restoring your seed restores your chat address.";
|
||||
strings_["chat_intro_fee"] = "Each message costs a small network fee. Share your chat address to let people start a conversation with you.";
|
||||
strings_["chat_intro_dismiss"] = "Got it";
|
||||
strings_["chat_empty_start"] = "Start one with \"New conversation\".";
|
||||
strings_["chat_copy_my_address"] = "Copy my chat address";
|
||||
strings_["chat_my_address_tip"] = "Share this address so others can message you";
|
||||
@@ -2126,6 +2130,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["export_tx_file_fail"] = "Failed to create CSV file";
|
||||
strings_["export_tx_none"] = "No transactions to export";
|
||||
strings_["export_tx_success"] = "Transactions exported successfully";
|
||||
strings_["export_tx_open_folder"] = "Open folder";
|
||||
strings_["export_tx_title"] = "Export Transactions to CSV";
|
||||
|
||||
// --- Import Key Dialog ---
|
||||
@@ -2177,6 +2182,8 @@ void I18n::loadBuiltinEnglish()
|
||||
|
||||
// --- Key Export Dialog ---
|
||||
strings_["key_export_fetching"] = "Fetching key from wallet...";
|
||||
strings_["key_export_fetching_base"] = "Fetching key from wallet";
|
||||
strings_["key_export_unlock_first"] = "Unlock your wallet first, then reveal the key.";
|
||||
strings_["key_export_failed"] = "Couldn't export the key \xE2\x80\x94 unlock the wallet (if encrypted) and try again.";
|
||||
strings_["key_export_private_key"] = "Private Key:";
|
||||
strings_["key_export_private_warning"] = "Keep this key SECRET! Anyone with this key can spend your funds. Never share it online or with untrusted parties.";
|
||||
@@ -2846,6 +2853,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["explorer_invalid_query"] = "Enter a block height or 64-character hash";
|
||||
strings_["explorer_invalid_response"] = "Invalid response from daemon";
|
||||
strings_["explorer_hash_not_found"] = "No block or transaction found for this hash";
|
||||
strings_["explorer_lookup_failed"] = "Lookup failed";
|
||||
strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash";
|
||||
strings_["explorer_no_results"] = "No matching cached blocks";
|
||||
strings_["explorer_beyond_tip"] = "Block %d is beyond the chain tip (%d)";
|
||||
|
||||
Reference in New Issue
Block a user