diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index eb759ac..63eb13d 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -106,6 +106,13 @@ std::string s_delete_name; // its peer name (for the confirm copy / bl bool s_show_blocked = false; // blocked-conversations manager overlay (unblock) bool s_show_restore = false; // restore-conversations overlay (reverse an on-chain delete) char s_restore_addr[128] = ""; // paste-a-chat-address buffer in the restore overlay +bool s_show_sensitive = false; // sensitive mark/unmark/setup/unlock overlay +std::string s_sensitive_cid; // target conversation (empty => "unlock the vault only", from the opener) +std::string s_sensitive_name; // peer display name (for the label) +std::string s_sensitive_peeraddr; // peer z-address (carried as the control label, for restore-by-address parity) +bool s_sensitive_unmark = false; // true => the dialog is UNmarking a currently-sensitive conversation +char s_sensitive_pass[128] = ""; // passphrase (setup / unlock) +char s_sensitive_pass2[128] = ""; // confirm passphrase (setup only) bool s_show_export_confirm = false; // "export as plaintext" confirm overlay std::string s_export_cid; // conversation targeted by the export confirm bool s_show_fingerprint = false; // key-fingerprint verification dialog @@ -976,6 +983,34 @@ void RenderChatTab(App* app) if (ImGui::SmallButton(rl.c_str())) s_show_restore = true; ImGui::PopStyleColor(); } + // Sensitive-vault opener: lock / unlock the local key for sensitive conversations. Shown only once + // a vault exists (it's created on the first "Make sensitive"). While locked it shows the count of + // sensitive conversations currently withheld. + { + const auto vs = app->chatSensitiveVaultState(); + if (vs != App::ChatVaultState::NotConfigured) { + if (blockedCount > 0 || onChainDelCount > 0) ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium()); + if (vs == App::ChatVaultState::Unlocked) { + if (ImGui::SmallButton(TR("chat_sensitive_lock_btn"))) { + app->lockChatSensitiveVault(); + s_convsKey = ~0ull; // sensitive threads vanish — rebuild the list + } + } else { // Locked + const int sn = app->settings() ? (int)app->settings()->sensitiveChatConversations().size() : 0; + const std::string ul = sn > 0 + ? std::string(TR("chat_sensitive_unlock_btn")) + " (" + std::to_string(sn) + ")" + : std::string(TR("chat_sensitive_unlock_btn")); + if (ImGui::SmallButton(ul.c_str())) { + s_sensitive_cid.clear(); s_sensitive_name.clear(); s_sensitive_peeraddr.clear(); + s_sensitive_unmark = false; + s_sensitive_pass[0] = '\0'; s_sensitive_pass2[0] = '\0'; + s_show_sensitive = true; // empty cid => unlock-only + } + } + ImGui::PopStyleColor(); + } + } // "Copy my chat address" — the address others use to reach this user. The conversation header // only copies a PEER's address, so without this there's no way to hand out your own. Copies on // click only (chatMyAddress may persist a freshly chosen address). @@ -1171,7 +1206,7 @@ void RenderChatTab(App* app) // The toolbar's left edge is known up front (from the button count). A rename (edit) icon is // shown whenever there's an address to save the contact under; the settings "notch" gear is // always the rightmost icon. - const int nBtns = 5 + (hasAddr ? 1 : 0); // export, mute, hide, delete, settings (+rename) + const int nBtns = 6 + (hasAddr ? 1 : 0); // export, mute, hide, sensitive, delete, settings (+rename) const float toolbarLeft = rightX - (nBtns * ib + (nBtns - 1) * gap); // Compact address + lock (or waiting-chip) metrics, reserved to the right of the name. @@ -1314,6 +1349,24 @@ void RenderChatTab(App* app) } bx += ib + gap; } + // Sensitive — per-conversation local-key encryption. Opens a dialog that (by vault state) + // sets up / unlocks the vault and marks or unmarks this conversation. + { + ImGui::SetCursorScreenPos(ImVec2(bx, by)); + material::IconButtonStyle a = base; + const bool isSens = app->chatService().isConversationSensitive(sel->cid); + a.tooltip = isSens ? TR("chat_sensitive_unmark_tip") : TR("chat_sensitive_mark_tip"); + if (material::IconButton("##hdr_sensitive", + isSens ? ICON_MD_LOCK : ICON_MD_ENHANCED_ENCRYPTION, ifont, ImVec2(ib, ib), a)) { + s_sensitive_cid = sel->cid; + s_sensitive_name = sel->peerName; + s_sensitive_peeraddr = sel->peerZaddr; + s_sensitive_unmark = isSens; + s_sensitive_pass[0] = '\0'; s_sensitive_pass2[0] = '\0'; + s_show_sensitive = true; + } + bx += ib + gap; + } // Delete — clears this conversation's LOCAL history. Destructive (and offers a // "delete & block" variant), so it opens a confirm dialog rather than acting inline. { @@ -2572,6 +2625,122 @@ void RenderChatTab(App* app) } } + // ---- Sensitive: setup / unlock / mark / unmark (per-conversation local-key encryption) ---- + if (s_show_sensitive) { + const float dp = Layout::dpiScale(); + const auto vs = app->chatSensitiveVaultState(); + const bool unlockOnly = s_sensitive_cid.empty(); + const bool needSetup = (vs == App::ChatVaultState::NotConfigured); + const bool needUnlock = (vs == App::ChatVaultState::Locked); + material::OverlayDialogSpec ov; + ov.title = unlockOnly ? TR("chat_sensitive_unlock_title") + : s_sensitive_unmark ? TR("chat_sensitive_unmark_title") + : TR("chat_sensitive_mark_title"); + ov.p_open = &s_show_sensitive; + ov.style = material::OverlayStyle::BlurFloat; + ov.cardWidth = 520.0f; ov.idSuffix = "chatsensitive"; + if (material::BeginOverlayDialog(ov)) { + ImGui::PushTextWrapPos(0.0f); + if (unlockOnly) { + ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium()); + ImGui::TextUnformatted(TR("chat_sensitive_unlock_body")); + ImGui::PopStyleColor(); + } else if (s_sensitive_unmark) { + ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium()); + ImGui::TextUnformatted(TR("chat_sensitive_unmark_body")); + ImGui::PopStyleColor(); + } else { + ImGui::TextUnformatted(TR("chat_sensitive_warn_1")); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + ImGui::PushStyleColor(ImGuiCol_Text, material::Error()); + ImGui::TextUnformatted(TR("chat_sensitive_warn_lose")); + ImGui::PopStyleColor(); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium()); + ImGui::TextUnformatted(TR("chat_sensitive_warn_dir")); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + ImGui::TextUnformatted(TR("chat_sensitive_warn_hist")); + ImGui::PopStyleColor(); + } + ImGui::PopTextWrapPos(); + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + + if (needSetup) { + ImGui::TextUnformatted(TR("chat_sensitive_setup_label")); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputTextWithHint("##senspass", TR("chat_sensitive_pass_hint"), s_sensitive_pass, + sizeof(s_sensitive_pass), ImGuiInputTextFlags_Password); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputTextWithHint("##senspass2", TR("chat_sensitive_pass2_hint"), s_sensitive_pass2, + sizeof(s_sensitive_pass2), ImGuiInputTextFlags_Password); + ImGui::Dummy(ImVec2(0, Layout::spacingMd())); + } else if (needUnlock) { + ImGui::TextUnformatted(TR("chat_sensitive_unlock_label")); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputTextWithHint("##senspass", TR("chat_sensitive_pass_hint"), s_sensitive_pass, + sizeof(s_sensitive_pass), ImGuiInputTextFlags_Password); + ImGui::Dummy(ImVec2(0, Layout::spacingMd())); + } + + const char* actLabel = + needSetup ? TR("chat_sensitive_btn_setup") + : needUnlock ? (unlockOnly ? TR("chat_sensitive_btn_unlock") + : s_sensitive_unmark ? TR("chat_sensitive_btn_unlock_unmark") + : TR("chat_sensitive_btn_unlock_mark")) + : (unlockOnly ? TR("chat_sensitive_btn_unlock") + : s_sensitive_unmark ? TR("chat_sensitive_btn_unmark") : TR("chat_sensitive_btn_mark")); + + auto closeWipe = [&]() { + std::memset(s_sensitive_pass, 0, sizeof(s_sensitive_pass)); + std::memset(s_sensitive_pass2, 0, sizeof(s_sensitive_pass2)); + s_show_sensitive = false; + s_sensitive_cid.clear(); s_sensitive_name.clear(); s_sensitive_peeraddr.clear(); + }; + + const float gap2 = Layout::spacingSm(); + const float wAct = std::max(160.0f * dp, ImGui::CalcTextSize(actLabel).x + 40.0f * dp); + const float wCan = std::max(90.0f * dp, ImGui::CalcTextSize(TR("chat_cancel")).x + 32.0f * dp); + material::BeginOverlayDialogFooter(wAct + wCan + gap2, /*drawSeparator=*/false); + const bool doAct = material::TactileButton(actLabel, ImVec2(wAct, 0)); + ImGui::SameLine(0, gap2); + const bool doCancel = material::TactileButton(TR("chat_cancel"), ImVec2(wCan, 0)); + + if (doAct) { + bool ok = true; + if (needSetup) { + const std::string p1(s_sensitive_pass), p2(s_sensitive_pass2); + if (p1.empty() || p1 != p2) { + Notifications::instance().error(TR("chat_sensitive_err_pass")); ok = false; + } else if (!app->createChatSensitiveVault(p1)) { + Notifications::instance().error(TR("chat_sensitive_err_create")); ok = false; + } + } else if (needUnlock) { + const std::string p1(s_sensitive_pass); + if (p1.empty() || !app->unlockChatSensitiveVault(p1)) { + Notifications::instance().error(TR("chat_sensitive_err_unlock")); ok = false; + } + } + if (ok) { + if (!unlockOnly) { + if (s_sensitive_unmark) { + app->unmarkChatConversationSensitive(s_sensitive_cid); + Notifications::instance().info(TR("chat_sensitive_toast_unmarked")); + } else { + app->markChatConversationSensitive(s_sensitive_cid, s_sensitive_peeraddr); + Notifications::instance().info(TR("chat_sensitive_toast_marked")); + } + } else { + Notifications::instance().info(TR("chat_sensitive_toast_unlocked")); + } + s_convsKey = ~0ull; // sensitive threads appear/disappear — rebuild the list + closeWipe(); + } + } + if (doCancel) closeWipe(); + material::EndOverlayDialog(); + } + } + // ---- Chat customization modal (opened by the header settings "notch") — house BlurFloat overlay ---- if (s_show_chat_settings) { material::OverlayDialogSpec ov; diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 8f937e8..d5fef45 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -331,6 +331,35 @@ void I18n::loadBuiltinEnglish() strings_["chat_restore_toast"] = "Restoring conversation - it reappears after the next scan"; strings_["chat_restore_paste_label"] = "Restore by chat address"; strings_["chat_restore_paste_hint"] = "Paste the conversation's chat (z) address"; + strings_["chat_sensitive_mark_tip"] = "Make sensitive"; + strings_["chat_sensitive_unmark_tip"] = "Sensitive - tap to change"; + strings_["chat_sensitive_mark_title"] = "Make this conversation sensitive?"; + strings_["chat_sensitive_unmark_title"] = "Make this conversation normal?"; + strings_["chat_sensitive_unlock_title"] = "Unlock sensitive conversations"; + strings_["chat_sensitive_warn_1"] = "Its messages will be encrypted on this device under a separate passphrase - not your seed phrase. Anyone who later restores your wallet from your seed alone won't see this conversation."; + strings_["chat_sensitive_warn_lose"] = "If you lose this passphrase, these messages are gone forever - they can't be recovered from your seed."; + strings_["chat_sensitive_warn_dir"] = "Your sent messages here are protected completely. Received messages are still on the blockchain and could, in theory, be recovered by someone who has your seed and other software - DragonX itself will never show them without your passphrase."; + strings_["chat_sensitive_warn_hist"] = "Messages already in this conversation stay recoverable from the chain; this fully protects messages from here on."; + strings_["chat_sensitive_unmark_body"] = "This conversation will no longer be encrypted under your sensitive-chat passphrase. New messages will be stored like any other conversation."; + strings_["chat_sensitive_unlock_body"] = "Enter your sensitive-chat passphrase to show your sensitive conversations again."; + strings_["chat_sensitive_setup_label"] = "Create a passphrase for sensitive conversations"; + strings_["chat_sensitive_unlock_label"] = "Enter your sensitive-chat passphrase"; + strings_["chat_sensitive_pass_hint"] = "Passphrase"; + strings_["chat_sensitive_pass2_hint"] = "Confirm passphrase"; + strings_["chat_sensitive_btn_setup"] = "Set up & make sensitive"; + strings_["chat_sensitive_btn_mark"] = "Make sensitive"; + strings_["chat_sensitive_btn_unmark"] = "Make normal"; + strings_["chat_sensitive_btn_unlock"] = "Unlock"; + strings_["chat_sensitive_btn_unlock_mark"] = "Unlock & make sensitive"; + strings_["chat_sensitive_btn_unlock_unmark"] = "Unlock & make normal"; + strings_["chat_sensitive_lock_btn"] = "Lock sensitive"; + strings_["chat_sensitive_unlock_btn"] = "Unlock sensitive"; + strings_["chat_sensitive_toast_marked"] = "Conversation is now sensitive"; + strings_["chat_sensitive_toast_unmarked"] = "Conversation is no longer sensitive"; + strings_["chat_sensitive_toast_unlocked"] = "Sensitive conversations unlocked"; + strings_["chat_sensitive_err_pass"] = "Passphrases don't match, or are empty."; + strings_["chat_sensitive_err_unlock"] = "Wrong passphrase."; + strings_["chat_sensitive_err_create"] = "Couldn't set up the sensitive-chat vault."; strings_["chat_pick_contact"] = "Choose from contacts\xE2\x80\xA6"; strings_["chat_no_z_contacts"] = "No shielded-address contacts yet"; strings_["chat_contact_needs_z"] = "Needs a shielded (z) address to chat";