feat(chat): confirm before exporting decrypted chat history to plaintext
The chat header's export button wrote a decrypted plain-text file immediately on click, warning only via a hover tooltip. It now opens a confirm dialog that states the file is unencrypted and shows the destination path before writing. The export format, path and write call are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+73
-17
@@ -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<unsigned char>(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<unsigned char>(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();
|
||||
|
||||
Reference in New Issue
Block a user