feat(chat): "Delete everywhere" action in the delete dialog (on-chain signal)
Adds a distinct, sync-gated "Delete everywhere" button to the conversation delete dialog: it blocks the thread locally (instant + persistent, so it can't flicker back mid-sync) AND broadcasts a self-addressed Control(del) via sendChatControl so every other device — and a future restore from the seed — hides it too. Carries the peer z-address as the control label so Restore can match by address. The button is disabled until the wallet is fully synced (lite uses its own sync-complete signal, since it never sets tip_known), with a note explaining the on-chain signal + fee. The two local-only modes (Delete / Delete & block) are unchanged. New English strings fall back cleanly until translated (chat_delete_everywhere*, chat_delete_needs_sync). Phase A, step 5 (delete UI). Restore dialog next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2342,6 +2342,57 @@ void RenderChatTab(App* app)
|
||||
s_delete_cid.clear(); s_delete_name.clear();
|
||||
};
|
||||
|
||||
// "Delete everywhere": block locally (instant, persistent — no sync flicker) AND broadcast a
|
||||
// self-addressed Control(del) so other devices + a fresh seed import hide it too. Carries the
|
||||
// peer z-address as the control label so Restore can match it by address.
|
||||
auto doDeleteEverywhere = [&]() {
|
||||
const std::string cid = s_delete_cid, name = s_delete_name;
|
||||
std::string peerAddr;
|
||||
for (const auto& m : app->chatService().store().conversation(cid)) {
|
||||
if (!m.peer_zaddr.empty()) { peerAddr = m.peer_zaddr; break; }
|
||||
}
|
||||
if (!app->chatService().deleteConversation(cid, /*block=*/true)) {
|
||||
Notifications::instance().error(TR("chat_delete_failed"));
|
||||
s_show_delete_confirm = false; s_delete_cid.clear(); s_delete_name.clear();
|
||||
return;
|
||||
}
|
||||
if (app->settings()) {
|
||||
app->settings()->setChatBlocked(cid, name, true);
|
||||
app->settings()->setChatHidden(cid, false);
|
||||
app->settings()->save();
|
||||
}
|
||||
app->sendChatControl(cid, "del", peerAddr.empty() ? name : peerAddr);
|
||||
if (s_selected_cid == cid) s_selected_cid.clear();
|
||||
Notifications::instance().info(TR("chat_delete_everywhere_toast"));
|
||||
s_show_delete_confirm = false; s_delete_cid.clear(); s_delete_name.clear();
|
||||
};
|
||||
|
||||
// Full-width on-chain delete above the local footer, gated on a fully-synced wallet (so a
|
||||
// conversation can't flicker in/out mid-sync while the delete signal is still catching up).
|
||||
// Full node requires tip_known (isSynced); lite never sets it, so use its own "sync complete
|
||||
// + at tip" signal there — otherwise the button would be permanently disabled in lite.
|
||||
const auto& syncSt = app->state().sync;
|
||||
const bool synced = app->isLiteBuild()
|
||||
? (!syncSt.syncing && syncSt.blocks > 0 && syncSt.blocks >= syncSt.headers - 2)
|
||||
: syncSt.isSynced();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Error(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Error()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Error(), 235)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnError()));
|
||||
ImGui::BeginDisabled(!synced);
|
||||
const bool doEvery = material::TactileButton(TR("chat_delete_everywhere"),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x, 0));
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor(4);
|
||||
if (doEvery) doDeleteEverywhere();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::TextUnformatted(synced ? TR("chat_delete_everywhere_note") : TR("chat_delete_needs_sync"));
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
auto textW = [&](const char* t) {
|
||||
return ImGui::CalcTextSize(t).x + ImGui::GetStyle().FramePadding.x * 2.0f + 20.0f * dp;
|
||||
};
|
||||
|
||||
@@ -312,6 +312,10 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_deleted_toast"] = "Conversation deleted";
|
||||
strings_["chat_delete_failed"] = "Couldn't delete the conversation — nothing was changed.";
|
||||
strings_["chat_blocked_toast"] = "Conversation deleted & blocked";
|
||||
strings_["chat_delete_everywhere"] = "Delete everywhere";
|
||||
strings_["chat_delete_everywhere_note"] = "Also tells your other devices - and a future restore from your seed phrase - to hide this conversation. It sends a tiny on-chain signal (costs the usual chat fee). You can bring it back later from Restore.";
|
||||
strings_["chat_delete_everywhere_toast"] = "Deleting everywhere - signal sent";
|
||||
strings_["chat_delete_needs_sync"] = "Wait until the wallet is fully synced to delete everywhere.";
|
||||
strings_["chat_blocked_manage"] = "Blocked";
|
||||
strings_["chat_blocked_title"] = "Blocked conversations";
|
||||
strings_["chat_blocked_desc"] = "Blocked conversations are removed and their messages are dropped — old and new — until you unblock. Unblocking re-imports the conversation from the chain.";
|
||||
|
||||
Reference in New Issue
Block a user