feat(chat): make the key-verify lock clickable with a fingerprint dialog

The header key-verify lock was an unexplained hover-only icon. It's now
clickable, opening a dialog that shows the contact's identity-key fingerprint
and explains comparing it out-of-band, with a Copy-key button and a clearer
tooltip. The crypto/verification logic is unchanged (the wording is careful that
the lock means "encrypted to this key", since there is no trusted/verified flag).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 23:35:32 -05:00
co-authored by Claude Opus 4.8
parent 534120d3de
commit a43ac0156f
+62 -4
View File
@@ -106,6 +106,8 @@ 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_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
std::string s_fingerprint_cid; // conversation targeted by the fingerprint dialog
// Export file path for a conversation (the same scheme the inline export used). // Export file path for a conversation (the same scheme the inline export used).
static std::string chatExportPathFor(const std::string& peerName) { static std::string chatExportPathFor(const std::string& peerName) {
@@ -1324,13 +1326,22 @@ void RenderChatTab(App* app)
// same row. Hidden while renaming (the field takes the whole span). // same row. Hidden while renaming (the field takes the whole span).
if (hasAddr && !renaming) { if (hasAddr && !renaming) {
float ax = textX + std::min(nameW, nameAvail) + nameSpacing; float ax = textX + std::min(nameW, nameAvail) + nameSpacing;
// Key-verify lock — we hold the peer's identity key; hover shows a comparable fingerprint. // Key-verify lock — we hold the peer's identity key; now clickable to open a dialog with the
// fingerprint for out-of-band comparison (the old hover-only lock was unexplained).
if (showLock) { if (showLock) {
const float ly = hp.y + (rowH - icoSz) * 0.5f; const float ly = hp.y + (rowH - icoSz) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(ax, ly));
const bool lockClicked = ImGui::InvisibleButton("##hdr_keylock",
ImVec2(std::max(1.0f, lockW - 5.0f * hdpi), icoSz));
const bool lockHov = ImGui::IsItemHovered();
hdl->AddText(icoFont, icoSz, ImVec2(ax, ly), hdl->AddText(icoFont, icoSz, ImVec2(ax, ly),
material::WithAlpha(material::Primary(), 220), ICON_MD_LOCK); lockHov ? material::Primary() : material::WithAlpha(material::Primary(), 220),
if (ImGui::IsMouseHoveringRect(ImVec2(ax, ly), ImVec2(ax + lockW, ly + icoSz))) ICON_MD_LOCK);
material::Tooltip("%s\n%s", TR("chat_verify_key"), keyFingerprint(sel->peerPubKey).c_str()); if (lockHov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("chat_verify_tip"));
}
if (lockClicked) { s_fingerprint_cid = sel->cid; s_show_fingerprint = true; }
ax += lockW; ax += lockW;
} }
// Click-to-copy compact address. // Click-to-copy compact address.
@@ -2207,6 +2218,53 @@ void RenderChatTab(App* app)
} }
} }
// ---- Key-fingerprint verification dialog ----
if (s_show_fingerprint) {
const ConvSummary* fp = nullptr;
for (const auto& c : convs) if (c.cid == s_fingerprint_cid) { fp = &c; break; }
if (!fp || fp->peerPubKey.empty()) {
s_show_fingerprint = false;
s_fingerprint_cid.clear();
} else {
material::OverlayDialogSpec ov;
ov.title = TR("chat_verify_title");
ov.p_open = &s_show_fingerprint;
ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = 500.0f;
ov.idSuffix = "chatfp";
if (material::BeginOverlayDialog(ov)) {
const float fdp = Layout::dpiScale();
const std::string fpStr = keyFingerprint(fp->peerPubKey);
ImGui::PushTextWrapPos(0.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR("chat_verify_body"));
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::TextWrapped("%s", fp->peerName.c_str());
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Primary()));
ImGui::TextWrapped("%s", fpStr.c_str());
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
const float fgap = ImGui::GetStyle().ItemSpacing.x;
float wCopy = std::max(110.0f * fdp, ImGui::CalcTextSize(TR("chat_verify_copy")).x + 24.0f * fdp);
float wClose = std::max(90.0f * fdp, ImGui::CalcTextSize(TR("chat_cancel")).x + 24.0f * fdp);
material::BeginOverlayDialogFooter(wCopy + wClose + fgap, false);
if (material::TactileButton(TR("chat_verify_copy"), ImVec2(wCopy, 0))) {
ImGui::SetClipboardText(fp->peerPubKey.c_str());
Notifications::instance().success(TR("copied"));
}
ImGui::SameLine(0, fgap);
if (material::TactileButton(TR("chat_cancel"), ImVec2(wClose, 0))) {
s_show_fingerprint = false;
s_fingerprint_cid.clear();
}
material::EndOverlayDialog();
}
}
}
// ---- Delete-conversation confirm (revive-on-new-message vs delete & block) ---- // ---- Delete-conversation confirm (revive-on-new-message vs delete & block) ----
if (s_show_delete_confirm) { if (s_show_delete_confirm) {
const float dp = Layout::dpiScale(); const float dp = Layout::dpiScale();