feat(chat): hide conversations + contact address picker for new conversation

Hide conversations:
- A "Hide" action in the thread header drops a conversation from the list. The
  messages stay in the seed-encrypted store (on-chain history can't be deleted);
  a new INCOMING message un-hides it (you can't un-receive), so nothing is lost.
- Hidden cids persist in settings (mirrors the mute list) and are skipped by both
  the conversation list and the unread badge.

New-conversation address picker:
- A "Choose from contacts" dropdown lists the address book's shielded (z-address)
  contacts and fills the recipient field on selection; manual paste still works.

8-language strings + CJK subset (+1 glyph 届).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 23:26:04 -05:00
parent fce873936e
commit fc414eeed4
14 changed files with 105 additions and 0 deletions

View File

@@ -216,6 +216,7 @@ void RenderChatTab(App* app)
// Build conversation summaries (single scan per conversation), sorted by most-recent activity.
std::vector<ConvSummary> convs;
for (const auto& cid : store.conversationIds()) {
if (app->settings() && app->settings()->isChatHidden(cid)) continue; // hidden — a new message un-hides it
const auto messages = store.conversation(cid);
if (messages.empty()) continue;
ConvSummary c;
@@ -439,6 +440,16 @@ void RenderChatTab(App* app)
app->settings()->setChatMuted(sel->cid, !muted);
app->settings()->save();
}
// Hide conversation — drops it from the list (messages stay in the encrypted store; a
// new incoming message brings it back). Deselect so the thread resets to the next one.
ImGui::SameLine();
if (ImGui::SmallButton(TR("chat_hide")) && app->settings()) {
app->markChatConversationSeen(sel->cid, sel->lastTs); // don't leave a phantom unread
app->settings()->setChatHidden(sel->cid, true);
app->settings()->save();
s_selected_cid.clear();
Notifications::instance().info(TR("chat_hidden_toast"));
}
}
ImGui::Separator();
@@ -620,6 +631,27 @@ void RenderChatTab(App* app)
if (material::BeginOverlayDialog(ov)) {
const float fieldW = ImGui::GetContentRegionAvail().x;
material::LabeledInput(TR("chat_new_zaddr"), "##newz", s_new_zaddr, sizeof(s_new_zaddr), fieldW);
// Or pick from contacts — chat needs a shielded z-address, so only z-addr contacts are listed.
// Selecting one fills the field above (manual paste still works).
ImGui::SetNextItemWidth(fieldW);
if (ImGui::BeginCombo("##newzpick", TR("chat_pick_contact"), ImGuiComboFlags_HeightLarge)) {
int shown = 0;
for (const auto& e : book.entries()) {
if (e.address.empty() || e.address[0] != 'z') continue; // chat requires a z-address
++shown;
const std::string item = e.label + " " + shorten(e.address, 16, 8);
if (ImGui::Selectable(item.c_str())) {
std::strncpy(s_new_zaddr, e.address.c_str(), sizeof(s_new_zaddr) - 1);
s_new_zaddr[sizeof(s_new_zaddr) - 1] = '\0';
}
}
if (shown == 0) {
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
ImGui::TextUnformatted(TR("chat_no_z_contacts"));
ImGui::PopStyleColor();
}
ImGui::EndCombo();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
material::LabeledInput(TR("chat_new_message"), "##newm", s_new_msg, sizeof(s_new_msg), fieldW);
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));