diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 3eba6de..80be222 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -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_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 +std::string s_fingerprint_cid; // conversation targeted by the fingerprint dialog // Export file path for a conversation (the same scheme the inline export used). 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). if (hasAddr && !renaming) { 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) { 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), - material::WithAlpha(material::Primary(), 220), ICON_MD_LOCK); - if (ImGui::IsMouseHoveringRect(ImVec2(ax, ly), ImVec2(ax + lockW, ly + icoSz))) - material::Tooltip("%s\n%s", TR("chat_verify_key"), keyFingerprint(sel->peerPubKey).c_str()); + lockHov ? material::Primary() : material::WithAlpha(material::Primary(), 220), + ICON_MD_LOCK); + 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; } // 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) ---- if (s_show_delete_confirm) { const float dp = Layout::dpiScale();