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_blocked = false; // blocked-conversations manager overlay (unblock)
|
||||||
bool s_show_restore = false; // restore-conversations overlay (reverse an on-chain delete)
|
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
|
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
|
bool s_show_export_confirm = false; // "export as plaintext" confirm overlay
|
||||||
std::string s_export_cid; // conversation targeted by the export confirm
|
std::string s_export_cid; // conversation targeted by the export confirm
|
||||||
bool s_show_fingerprint = false; // key-fingerprint verification dialog
|
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;
|
if (ImGui::SmallButton(rl.c_str())) s_show_restore = true;
|
||||||
ImGui::PopStyleColor();
|
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
|
// "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
|
// 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).
|
// 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
|
// 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
|
// shown whenever there's an address to save the contact under; the settings "notch" gear is
|
||||||
// always the rightmost icon.
|
// 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);
|
const float toolbarLeft = rightX - (nBtns * ib + (nBtns - 1) * gap);
|
||||||
|
|
||||||
// Compact address + lock (or waiting-chip) metrics, reserved to the right of the name.
|
// 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;
|
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 — clears this conversation's LOCAL history. Destructive (and offers a
|
||||||
// "delete & block" variant), so it opens a confirm dialog rather than acting inline.
|
// "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 ----
|
// ---- Chat customization modal (opened by the header settings "notch") — house BlurFloat overlay ----
|
||||||
if (s_show_chat_settings) {
|
if (s_show_chat_settings) {
|
||||||
material::OverlayDialogSpec ov;
|
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_toast"] = "Restoring conversation - it reappears after the next scan";
|
||||||
strings_["chat_restore_paste_label"] = "Restore by chat address";
|
strings_["chat_restore_paste_label"] = "Restore by chat address";
|
||||||
strings_["chat_restore_paste_hint"] = "Paste the conversation's chat (z) 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_pick_contact"] = "Choose from contacts\xE2\x80\xA6";
|
||||||
strings_["chat_no_z_contacts"] = "No shielded-address contacts yet";
|
strings_["chat_no_z_contacts"] = "No shielded-address contacts yet";
|
||||||
strings_["chat_contact_needs_z"] = "Needs a shielded (z) address to chat";
|
strings_["chat_contact_needs_z"] = "Needs a shielded (z) address to chat";
|
||||||
|
|||||||
Reference in New Issue
Block a user