From 7cf0bb8bc75908483df10efa7b1b6e58d2682503 Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 21 Jul 2026 21:31:15 -0500 Subject: [PATCH] feat(settings): add hover tooltips to the settings controls that lacked them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A per-card audit found 38 interactive controls with no tooltip. Add them, with new tt_* i18n keys (English source; per-language JSONs fall back to English until translated): - Chat & Contacts (8): emoji/bubble style, accent, density, text size, poll rate, timestamps, enter-sends — the segmented/beginRow helpers added none. - Node & Security (26): the whole lite wallet lifecycle / backup-keys / encryption panel, plus the full-node RPC-details toggle and daemon Refresh. - Debug Options (4): the screenshot-sweep / full-UI-sweep / open-folder / seed-demo-chat buttons. Theme, Wallet, Explorer, About were already fully covered. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/pages/settings_page.cpp | 34 ++++++++++++++++++++++++++-- src/ui/windows/chat_tab.cpp | 8 +++++++ src/util/i18n.cpp | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 68e857e..7283f24 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1646,6 +1646,7 @@ void RenderSettingsPage(App* app) { if (material::TactileButton(TrId("lite_wallet_request", "LiteLifecycleToggle").c_str(), ImVec2(liteInputW, 0))) { s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded; } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_toggle")); if (s_settingsState.lite_lifecycle_expanded) { const char* lifecycleLabels[] = {TR("lite_op_create"), TR("lite_op_open"), TR("lite_op_restore")}; @@ -1667,6 +1668,7 @@ void RenderSettingsPage(App* app) { } ImGui::EndCombo(); } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_op")); if (s_settingsState.lite_lifecycle_operation == 1 || s_settingsState.lite_lifecycle_operation == 2) { @@ -1677,6 +1679,7 @@ void RenderSettingsPage(App* app) { ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path, sizeof(s_settingsState.lite_wallet_path)); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_wallet_path")); } if (s_settingsState.lite_lifecycle_operation == 2) { @@ -1688,6 +1691,7 @@ void RenderSettingsPage(App* app) { ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed, sizeof(s_settingsState.lite_restore_seed), ImGuiInputTextFlags_Password); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_seed")); // Live 24-word count so the user knows the seed is complete before restoring. { const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed); @@ -1703,6 +1707,7 @@ void RenderSettingsPage(App* app) { ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(std::min(160.0f, liteInputW)); ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_birthday")); if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0; ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), @@ -1714,11 +1719,13 @@ void RenderSettingsPage(App* app) { ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW); ImGui::SetNextItemWidth(std::min(160.0f, liteInputW)); ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_account")); if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0; ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::Checkbox(TrId("lite_overwrite", "LiteRestoreOverwrite").c_str(), &s_settingsState.lite_restore_overwrite); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_overwrite")); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); @@ -1730,6 +1737,7 @@ void RenderSettingsPage(App* app) { s_settingsState.lite_lifecycle_passphrase, sizeof(s_settingsState.lite_lifecycle_passphrase), ImGuiInputTextFlags_Password); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_pass")); // Poll a completed async create/open/restore (driven by the App-owned // controller; finalized on the main thread by App::update's @@ -1760,6 +1768,7 @@ void RenderSettingsPage(App* app) { if (TactileButton(TrId("lite_validate", "LiteLifecycleValidate").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { evaluateLiteLifecycleRequestFromPageState(app); } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_lite_lifecycle_run")); if (liteLifecycleBusy) ImGui::EndDisabled(); if (!s_settingsState.lite_lifecycle_status.empty()) { @@ -1806,6 +1815,7 @@ void RenderSettingsPage(App* app) { } wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_show_seed")); ImGui::SameLine(0, Layout::spacingSm()); if (material::ActionButton("##LiteExportKeys", TR("lite_show_private_keys"), ICON_MD_KEY, material::ActionTier::Secondary)) { auto r = app->liteWallet()->exportPrivateKeys(); @@ -1821,6 +1831,7 @@ void RenderSettingsPage(App* app) { } wallet::secureWipeLiteSecret(r.privateKeysJson); } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_show_keys")); // Revealed secret: shown read-only (no extra copies), with copy + wipe. if (!s_settingsState.lite_export_secret.empty()) { @@ -1843,6 +1854,7 @@ void RenderSettingsPage(App* app) { if (TactileButton(TrId("lite_copy", "LiteExportCopy").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { app->copySecretToClipboard(s_settingsState.lite_export_secret); } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_copy")); ImGui::SameLine(0, Layout::spacingSm()); // Save the seed (+ birthday) to an owner-only (0600) file in the config dir. if (s_settingsState.lite_export_is_seed && @@ -1857,12 +1869,16 @@ void RenderSettingsPage(App* app) { ? (std::string(TR("lite_saved_to")) + path) : (std::string(TR("lite_could_not_write")) + path); } - if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm()); + if (s_settingsState.lite_export_is_seed) { + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_save_seed_file")); + ImGui::SameLine(0, Layout::spacingSm()); + } if (TactileButton(TrId("lite_hide_wipe", "LiteExportHide").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); s_settingsState.lite_export_label.clear(); s_settingsState.lite_export_is_seed = false; } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_hide_wipe")); } // Import a spending/viewing key (history appears after the next sync). @@ -1874,6 +1890,7 @@ void RenderSettingsPage(App* app) { ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key), ImGuiInputTextFlags_Password); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_import_key")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_import", "LiteImportKeyBtn").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { std::string liteKey(s_settingsState.lite_import_key); @@ -1888,6 +1905,7 @@ void RenderSettingsPage(App* app) { r.ok ? TR("lite_key_imported") : r.error; } } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_import_key_btn")); if (!s_settingsState.lite_backup_status.empty()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); @@ -1909,12 +1927,14 @@ void RenderSettingsPage(App* app) { ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_encrypt_pass")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_encrypt_wallet", "LiteEncrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass); sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); s_settingsState.lite_encryption_status = r.ok ? TR("lite_wallet_encrypted") : r.error; } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_encrypt")); } else { if (wstate.isLocked()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); @@ -1924,12 +1944,14 @@ void RenderSettingsPage(App* app) { ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock_pass")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_unlock", "LiteUnlock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass); sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass)); s_settingsState.lite_encryption_status = ok ? TR("lite_wallet_unlocked") : TR("lite_unlock_failed"); } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock")); } else { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_lock_now", "LiteLock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { @@ -1937,6 +1959,7 @@ void RenderSettingsPage(App* app) { s_settingsState.lite_encryption_status = app->lockLiteWallet() ? TR("lite_wallet_locked") : TR("lite_lock_failed"); } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lock")); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); @@ -1945,12 +1968,14 @@ void RenderSettingsPage(App* app) { ImGui::SetNextItemWidth(liteInputW); ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_decrypt_pass")); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_remove_encryption", "LiteDecrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass); sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass)); s_settingsState.lite_encryption_status = r.ok ? TR("lite_encryption_removed") : r.error; } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_remove_encrypt")); } if (!s_settingsState.lite_encryption_status.empty()) { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); @@ -2096,7 +2121,7 @@ void RenderSettingsPage(App* app) { ImVec2 hdrPos = ImGui::GetCursorScreenPos(); if (ImGui::Button("##RpcToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) s_settingsState.rpc_expanded = !s_settingsState.rpc_expanded; - if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", TR("tt_rpc_toggle")); } float textY = hdrPos.y + (ImGui::GetFrameHeight() - rpcHdrFont->LegacySize) * 0.5f; dl->AddText(rpcHdrFont, rpcHdrFont->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("rpc_connection")); ImFont* rpcIconFont = Type().iconSmall(); @@ -2288,6 +2313,7 @@ void RenderSettingsPage(App* app) { uf.next(material::ActionButtonWidth(TR("refresh"), ICON_MD_REFRESH)); if (material::ActionButton("##drefresh", TR("refresh"), ICON_MD_REFRESH, AT::Secondary)) s_settingsState.daemon_info_loaded = false; + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_daemon_refresh")); ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available); uf.next(material::ActionButtonWidth(TR("daemon_install_bundled"), ICON_MD_DOWNLOAD)); if (material::ActionButton("##dinst", TR("daemon_install_bundled"), ICON_MD_DOWNLOAD, AT::Secondary)) @@ -2619,18 +2645,22 @@ void RenderSettingsPage(App* app) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button"))) app->startScreenshotSweep(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_sweep")); ImGui::SameLine(); // Full UI sweep: also captures every modal / dialog / flow / state (demo data, offline). if (TactileButton(TR("screenshot_sweep_full"), ImVec2(0, 0), S.resolveFont("button"))) app->startFullUiSweep(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_sweep_full")); ImGui::SameLine(); if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button"))) util::Platform::openFolder(app->screenshotDir()); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_open_dir")); if (chat::hushChatFeatureEnabledAtBuild()) { // Populate the Chat tab with demo conversations so the sweep captures its real UI. ImGui::SameLine(); if (TactileButton("Seed demo chat", ImVec2(0, 0), S.resolveFont("button"))) app->seedChatDemoData(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat")); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Separator(); diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 22acc48..3539252 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -1946,6 +1946,7 @@ void RenderChatSettingsControls(App* app, float contentWidth) int v = st->getChatEmojiColor() ? 1 : 0; const char* items[] = { TR("chat_emoji_mono"), TR("chat_emoji_color") }; int nv = segmented(TR("chat_opt_emoji"), items, 2, v); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_emoji_style")); if (nv != v) { st->setChatEmojiColor(nv == 1); st->save(); app->requestFontRebuild(); } } // Bubble style (segmented) + accent color (a 6-way dropdown — too many for a segmented control). @@ -1953,6 +1954,7 @@ void RenderChatSettingsControls(App* app, float contentWidth) const char* items[] = { TR("chat_bubble_rounded"), TR("chat_bubble_square"), TR("chat_bubble_minimal") }; int v = st->getChatBubbleStyle(); int nv = segmented(TR("chat_opt_bubble_style"), items, 3, v); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_bubble_style")); if (nv != v) { st->setChatBubbleStyle(nv); st->save(); } } { @@ -1961,12 +1963,14 @@ void RenderChatSettingsControls(App* app, float contentWidth) const char* items[] = { TR("chat_accent_theme"), TR("chat_accent_blue"), TR("chat_accent_green"), TR("chat_accent_purple"), TR("chat_accent_amber"), TR("chat_accent_pink") }; if (ImGui::Combo("##chat_baccent", &v, items, 6)) { st->setChatBubbleAccent(v); st->save(); } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_bubble_accent")); } // Message density (segmented). { const char* items[] = { TR("chat_density_comfortable"), TR("chat_density_compact") }; int v = st->getChatDensity(); int nv = segmented(TR("chat_opt_density"), items, 2, v); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_density")); if (nv != v) { st->setChatDensity(nv); st->save(); } } // Message text size (slider). @@ -1976,6 +1980,7 @@ void RenderChatSettingsControls(App* app, float contentWidth) if (ImGui::SliderFloat("##chat_font", &v, 0.8f, 1.5f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) { st->setChatFontScale(v); st->save(); } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_font_size")); } // Move Messaging into the right column (float it with Indent so every row's @@ -1996,12 +2001,14 @@ void RenderChatSettingsControls(App* app, float contentWidth) if (ImGui::SliderFloat("##chat_poll", &v, 0.5f, 15.0f, "%.1f s", ImGuiSliderFlags_AlwaysClamp)) { st->setChatPollRateSec(v); st->save(); } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_poll_rate")); } // Chat timestamps (segmented) — overrides the app-wide clock (Settings → General) for this tab only. { const char* items[] = { TR("chat_ts_global_short"), TR("chat_ts_24h"), TR("chat_ts_12h") }; int v = st->getChatTimeFormat(); int nv = segmented(TR("chat_opt_timestamp"), items, 3, v); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_timestamp")); if (nv != v) { st->setChatTimeFormat(nv); st->save(); } } // Enter-to-send (checkbox). @@ -2009,6 +2016,7 @@ void RenderChatSettingsControls(App* app, float contentWidth) ImGui::Dummy(ImVec2(0.0f, rowGap)); bool v = st->getChatEnterSends(); if (ImGui::Checkbox(TR("chat_opt_enter_sends"), &v)) { st->setChatEnterSends(v); st->save(); } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_enter_sends")); } // Close the two-column band: un-indent and drop below the taller column. diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 52de3cd..b49b19e 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -721,6 +721,47 @@ void I18n::loadBuiltinEnglish() strings_["tt_idle_delay"] = "How long to wait before starting mining"; strings_["tt_wizard"] = "Re-run the initial setup wizard\nDaemon will be restarted"; strings_["tt_download_bootstrap"] = "Download blockchain bootstrap to speed up sync\nExisting block data will be replaced"; + // --- Full-node Node & Security tooltips --- + strings_["tt_rpc_toggle"] = "Show or hide the read-only RPC connection details (host, port, user, password) for the daemon"; + strings_["tt_daemon_refresh"] = "Re-read the installed and bundled dragonxd version, size, and date shown above"; + // --- Lite wallet Node & Security tooltips --- + strings_["tt_lite_lifecycle_toggle"] = "Show or hide the create / open / restore controls for managing your lite wallet file"; + strings_["tt_lite_lifecycle_op"] = "Choose whether to create a new wallet, open an existing one, or restore one from a seed phrase"; + strings_["tt_lite_wallet_path"] = "Path or name of the wallet file to open or restore into"; + strings_["tt_lite_restore_seed"] = "The 24-word recovery seed phrase to restore this wallet from; hidden as you type"; + strings_["tt_lite_restore_birthday"] = "Block height the wallet was created at; scanning starts here. Use 0 or the earliest height if unsure"; + strings_["tt_lite_restore_account"] = "HD account index to restore; leave 0 unless you used multiple accounts under this seed"; + strings_["tt_lite_restore_overwrite"] = "Replace an existing wallet file with this restore. Warning: overwrites the current wallet data"; + strings_["tt_lite_lifecycle_pass"] = "Passphrase to unlock or set on the wallet during this create / open / restore operation"; + strings_["tt_lite_lifecycle_run"] = "Run the selected create / open / restore operation with the values above"; + strings_["tt_lite_show_seed"] = "Reveal this wallet's recovery seed phrase and birthday. Anyone with the seed can spend your funds"; + strings_["tt_lite_show_keys"] = "Reveal this wallet's private spending keys. Anyone with a key can spend the funds it controls"; + strings_["tt_lite_copy"] = "Copy the revealed secret to the clipboard"; + strings_["tt_lite_save_seed_file"] = "Write the seed and birthday to an owner-only file (lite-seed-backup.txt) in the config folder"; + strings_["tt_lite_hide_wipe"] = "Hide the revealed secret and securely wipe it from memory"; + strings_["tt_lite_import_key"] = "Paste a private spending or viewing key to import; its history appears after the next sync"; + strings_["tt_lite_import_key_btn"] = "Import the entered private key into this wallet; funds and history appear after the next sync"; + strings_["tt_lite_encrypt_pass"] = "Passphrase to encrypt the wallet with. If lost, the wallet cannot be unlocked or recovered"; + strings_["tt_lite_encrypt"] = "Encrypt the wallet with the passphrase above; it locks immediately and requires the passphrase to unlock"; + strings_["tt_lite_unlock_pass"] = "Enter your passphrase to unlock the encrypted wallet"; + strings_["tt_lite_unlock"] = "Unlock the encrypted wallet using the passphrase above"; + strings_["tt_lite_lock"] = "Lock the wallet now; a passphrase is required to unlock and any chat session is torn down"; + strings_["tt_lite_decrypt_pass"] = "Enter your passphrase to remove encryption from the wallet"; + strings_["tt_lite_remove_encrypt"] = "Remove encryption and store the wallet unprotected; no passphrase will be required to open it"; + // --- Chat & Contacts tooltips --- + strings_["tt_chat_emoji_style"] = "Render emoji in monochrome outline or full color"; + strings_["tt_chat_bubble_style"] = "Message bubble shape: rounded, square, or minimal (flat, borderless)"; + strings_["tt_chat_bubble_accent"] = "Accent color for your outgoing message bubbles (or follow the current theme)"; + strings_["tt_chat_density"] = "Spacing between messages: Comfortable adds more padding; Compact fits more on screen"; + strings_["tt_chat_font_size"] = "Scale chat message text from 0.8x to 1.5x. Affects only the Chat tab, not the rest of the app"; + strings_["tt_chat_poll_rate"] = "How often to check for new and 0-conf messages (0.5-15 s). Faster is more responsive but uses more CPU"; + strings_["tt_chat_timestamp"] = "Timestamp format for this tab only: follow the app-wide clock, or force 24-hour or 12-hour"; + strings_["tt_chat_enter_sends"] = "When on, Enter sends the message and Shift+Enter adds a newline; when off, Enter adds a newline"; + // --- Debug Options tooltips --- + strings_["tt_screenshot_sweep"] = "Cycle every theme across every tab, saving a screenshot of each into the config screenshots folder (overwrites the last sweep)"; + strings_["tt_screenshot_sweep_full"] = "Like the theme sweep but also captures every modal / dialog / flow using temporary offline demo wallet data"; + strings_["tt_screenshot_open_dir"] = "Open the screenshots folder (under the config directory) in your file manager"; + strings_["tt_seed_demo_chat"] = "Inject sample conversations into the Chat tab so a sweep captures its UI; in-memory only, gone on restart"; strings_["download_bootstrap"] = "Download Bootstrap"; strings_["download"] = "Download"; strings_["retry"] = "Retry";