feat(chat): sensitive-conversation UI (mark/unmark, setup/unlock, lock)
Wires sensitive mode into the chat tab: - A per-conversation lock toggle in the header (next to delete) opens a dialog that, by vault state, sets up / unlocks the vault and marks or unmarks the conversation. The mark dialog shows the honest warning (lose the passphrase = gone forever; sent fully protected vs received still chain-recoverable; prior history stays recoverable). - A "Lock sensitive" / "Unlock sensitive (N)" opener beside Blocked/Restore locks or unlocks the vault; unlocking surfaces the withheld sensitive threads. - Passphrase fields use the password flag and are memset on close; marking requires the vault unlocked (the App layer migrates rows + broadcasts the on-chain signal). ~29 new English strings (fall back to English until translated). Phase B complete (policy level); the cryptographic re-key level remains out of scope. Needs a live smoke test (built unverified, like the delete UI). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+170
-1
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user