feat(chat): Restore dialog for on-chain-deleted conversations
Adds a "Restore" opener (shown when a delete is known from chain — so it works
on a wallet freshly restored from seed — or something is locally blocked) and a
Restore dialog that: lists chain-derived deleted conversations
(onChainDeletedConversations) each with a Restore button; accepts a pasted chat
(z) address, matched against the address each delete signal carries (else
treated as a raw cid); and offers Quick scan (noteWalletMutation → re-run the
change-gated scans → re-harvest control memos). Restoring clears any local
block/hide and broadcasts Control("res") so every device un-hides; the messages
re-import from chain on the next scan.
New English strings fall back cleanly until translated (chat_restore_*).
Phase A complete: protocol -> compose -> reducer -> broadcast -> delete+restore UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,8 @@ 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_restore = false; // restore-conversations overlay (reverse an on-chain delete)
|
||||
char s_restore_addr[128] = ""; // paste-a-chat-address buffer in the restore overlay
|
||||
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
|
||||
@@ -961,6 +963,19 @@ void RenderChatTab(App* app)
|
||||
if (ImGui::SmallButton(bl.c_str())) s_show_blocked = true;
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
// Restore opener — reverse an on-chain "Delete everywhere". Shown when a delete is known from chain
|
||||
// (works on a fresh seed import, which has no local state) or something is locally blocked; the
|
||||
// dialog also offers paste-by-address + a quick scan for deletes made on another device.
|
||||
const int onChainDelCount = (int)app->chatService().onChainDeletedConversations().size();
|
||||
if (onChainDelCount > 0 || blockedCount > 0) {
|
||||
if (blockedCount > 0) ImGui::SameLine();
|
||||
const std::string rl = onChainDelCount > 0
|
||||
? std::string(TR("chat_restore_manage")) + " (" + std::to_string(onChainDelCount) + ")"
|
||||
: std::string(TR("chat_restore_manage"));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
if (ImGui::SmallButton(rl.c_str())) s_show_restore = true;
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
// "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
|
||||
// click only (chatMyAddress may persist a freshly chosen address).
|
||||
@@ -2464,6 +2479,99 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Restore conversations (reverse an on-chain "Delete everywhere") ----
|
||||
if (s_show_restore) {
|
||||
const float dp = Layout::dpiScale();
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("chat_restore_title");
|
||||
ov.p_open = &s_show_restore;
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 520.0f; ov.idSuffix = "chatrestore";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::TextUnformatted(TR("chat_restore_desc"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Restore: clear any local block/hide here AND broadcast Control("res") so every device un-hides.
|
||||
// The messages re-import from the chain on the next scan (the reducer no longer hides the cid).
|
||||
auto doRestore = [&](const std::string& cid, const std::string& label) {
|
||||
if (cid.empty()) return;
|
||||
if (app->settings()) {
|
||||
app->settings()->setChatBlocked(cid, "", false);
|
||||
app->settings()->setChatHidden(cid, false);
|
||||
app->settings()->save();
|
||||
}
|
||||
app->forgetChatConversationSeen(cid);
|
||||
app->sendChatControl(cid, "res", label.empty() ? cid : label);
|
||||
Notifications::instance().info(TR("chat_restore_toast"));
|
||||
};
|
||||
|
||||
// Chain-derived deleted conversations — present even on a wallet freshly restored from seed.
|
||||
const auto deleted = app->chatService().onChainDeletedConversations();
|
||||
if (deleted.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextUnformatted(TR("chat_restore_none"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
std::string restoreCid, restoreLabel;
|
||||
for (const auto& d : deleted) {
|
||||
ImGui::PushID(d.first.c_str());
|
||||
const std::string label = d.second.empty() ? shorten(d.first, 10, 6) : shorten(d.second, 12, 6);
|
||||
const float bw = ImGui::CalcTextSize(TR("chat_restore_btn")).x +
|
||||
ImGui::GetStyle().FramePadding.x * 2.0f + 16.0f * dp;
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - bw);
|
||||
if (material::TactileButton(TR("chat_restore_btn"), ImVec2(bw, 0))) {
|
||||
restoreCid = d.first; restoreLabel = d.second;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!restoreCid.empty()) doRestore(restoreCid, restoreLabel);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Paste the chat (z) address to restore a conversation not in the list (matched against the
|
||||
// address each delete signal carries; otherwise the input is treated as a raw conversation id).
|
||||
ImGui::TextUnformatted(TR("chat_restore_paste_label"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
ImGui::InputTextWithHint("##chatrestoreaddr", TR("chat_restore_paste_hint"),
|
||||
s_restore_addr, sizeof(s_restore_addr));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
const float gap = Layout::spacingSm();
|
||||
const float wPaste = std::max(120.0f * dp, ImGui::CalcTextSize(TR("chat_restore_btn")).x + 44.0f * dp);
|
||||
const float wScan = std::max(120.0f * dp, ImGui::CalcTextSize(TR("chat_restore_scan")).x + 44.0f * dp);
|
||||
if (material::TactileButton(TR("chat_restore_btn"), ImVec2(wPaste, 0))) {
|
||||
std::string input(s_restore_addr);
|
||||
while (!input.empty() && (input.front() == ' ' || input.front() == '\n' || input.front() == '\t')) input.erase(input.begin());
|
||||
while (!input.empty() && (input.back() == ' ' || input.back() == '\n' || input.back() == '\t')) input.pop_back();
|
||||
if (!input.empty()) {
|
||||
std::string cid, label;
|
||||
for (const auto& d : deleted) { if (d.second == input) { cid = d.first; label = d.second; break; } }
|
||||
if (cid.empty()) { cid = input; label = input; } // not in the list — treat as a raw cid
|
||||
doRestore(cid, label);
|
||||
s_restore_addr[0] = '\0';
|
||||
}
|
||||
}
|
||||
ImGui::SameLine(0, gap);
|
||||
if (material::TactileButton(TR("chat_restore_scan"), ImVec2(wScan, 0))) {
|
||||
app->noteWalletMutation(); // re-run the change-gated scans → re-harvest control memos
|
||||
Notifications::instance().info(TR("chat_restore_scanning"));
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Chat customization modal (opened by the header settings "notch") — house BlurFloat overlay ----
|
||||
if (s_show_chat_settings) {
|
||||
material::OverlayDialogSpec ov;
|
||||
|
||||
Reference in New Issue
Block a user