diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 3b75eec..3eba6de 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -104,6 +104,16 @@ bool s_show_delete_confirm = false; // "Delete conversation?" confirm overlay ( std::string s_delete_cid; // conversation targeted by the delete confirm std::string s_delete_name; // its peer name (for the confirm copy / block-list label) 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 + +// Export file path for a conversation (the same scheme the inline export used). +static std::string chatExportPathFor(const std::string& peerName) { + std::string safe; + for (char ch : peerName) safe += (std::isalnum(static_cast(ch)) ? ch : '_'); + if (safe.empty()) safe = "chat"; + return util::Platform::getConfigDir() + "/dragonx-chat-" + safe + ".txt"; +} bool s_show_emoji_picker = false; // emoji picker overlay — fills the conversation-list pane while open char s_emoji_search[48] = ""; // emoji picker keyword filter @@ -1241,23 +1251,9 @@ void RenderChatTab(App* app) ImGui::SetCursorScreenPos(ImVec2(bx, by)); material::IconButtonStyle a = base; a.tooltip = TR("chat_export_warn"); if (material::IconButton("##hdr_export", ICON_MD_FILE_DOWNLOAD, ifont, ImVec2(ib, ib), a)) { - std::string content = "DragonX chat export\n"; - content += sel->peerName + " <" + sel->peerZaddr + ">\n"; - content += "cid: " + sel->cid + "\n\n"; - for (const auto& m : store.conversation(sel->cid)) { - const bool out = (m.direction == chat::ChatDirection::Outgoing); - content += "[" + formatTime(m.timestamp) + "] " + - (out ? std::string(TR("chat_you")) : sel->peerName) + ": " + m.body + "\n"; - } - std::string safe; - for (char ch : sel->peerName) - safe += (std::isalnum(static_cast(ch)) ? ch : '_'); - if (safe.empty()) safe = "chat"; - const std::string path = util::Platform::getConfigDir() + "/dragonx-chat-" + safe + ".txt"; - if (util::Platform::writeFileAtomically(path, content, /*restrictPermissions=*/true)) - Notifications::instance().success(std::string(TR("chat_export_done")) + ": " + path); - else - Notifications::instance().error(TR("chat_export_failed")); + // The export writes DECRYPTED plaintext — confirm first instead of writing on one click. + s_export_cid = sel->cid; + s_show_export_confirm = true; } bx += ib + gap; } @@ -2151,6 +2147,66 @@ void RenderChatTab(App* app) } } + // ---- Export-as-plaintext confirm (unencrypted file on disk) ---- + if (s_show_export_confirm) { + const ConvSummary* ex = nullptr; + for (const auto& c : convs) if (c.cid == s_export_cid) { ex = &c; break; } + if (!ex) { + s_show_export_confirm = false; + s_export_cid.clear(); + } else { + material::OverlayDialogSpec ov; + ov.title = TR("chat_export_confirm_title"); + ov.p_open = &s_show_export_confirm; + ov.style = material::OverlayStyle::BlurFloat; + ov.cardWidth = 500.0f; + ov.idSuffix = "chatexport"; + if (material::BeginOverlayDialog(ov)) { + const float edp = Layout::dpiScale(); + const std::string path = chatExportPathFor(ex->peerName); + ImGui::PushTextWrapPos(0.0f); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning())); + ImGui::TextWrapped("%s", TR("chat_export_confirm_body")); + ImGui::PopStyleColor(); + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium())); + ImGui::TextUnformatted(TR("chat_export_confirm_path")); + ImGui::PopStyleColor(); + ImGui::TextWrapped("%s", path.c_str()); + ImGui::PopTextWrapPos(); + ImGui::Dummy(ImVec2(0, Layout::spacingMd())); + + const float egap = ImGui::GetStyle().ItemSpacing.x; + float wExp = std::max(110.0f * edp, ImGui::CalcTextSize(TR("chat_export_confirm_btn")).x + 24.0f * edp); + float wCan = std::max(90.0f * edp, ImGui::CalcTextSize(TR("chat_cancel")).x + 24.0f * edp); + material::BeginOverlayDialogFooter(wExp + wCan + egap, false); + if (material::TactileButton(TR("chat_export_confirm_btn"), ImVec2(wExp, 0))) { + // Build + write exactly as the old inline export did (format byte-identical). + std::string content = "DragonX chat export\n"; + content += ex->peerName + " <" + ex->peerZaddr + ">\n"; + content += "cid: " + ex->cid + "\n\n"; + for (const auto& m : store.conversation(ex->cid)) { + const bool out = (m.direction == chat::ChatDirection::Outgoing); + content += "[" + formatTime(m.timestamp) + "] " + + (out ? std::string(TR("chat_you")) : ex->peerName) + ": " + m.body + "\n"; + } + if (util::Platform::writeFileAtomically(path, content, /*restrictPermissions=*/true)) + Notifications::instance().success(std::string(TR("chat_export_done")) + ": " + path); + else + Notifications::instance().error(TR("chat_export_failed")); + s_show_export_confirm = false; + s_export_cid.clear(); + } + ImGui::SameLine(0, egap); + if (material::TactileButton(TR("chat_cancel"), ImVec2(wCan, 0))) { + s_show_export_confirm = false; + s_export_cid.clear(); + } + material::EndOverlayDialog(); + } + } + } + // ---- Delete-conversation confirm (revive-on-new-message vs delete & block) ---- if (s_show_delete_confirm) { const float dp = Layout::dpiScale();