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

Binary file not shown.

View File

@@ -146,6 +146,8 @@
"chat_export_done": "Unterhaltung exportiert",
"chat_export_failed": "Exportdatei konnte nicht geschrieben werden.",
"chat_export_warn": "Speichert die entschlüsselten Nachrichten als Klartext. Bewahre die Datei sicher auf.",
"chat_hidden_toast": "Unterhaltung ausgeblendet eine neue Nachricht holt sie zurück",
"chat_hide": "Ausblenden",
"chat_jump_latest": "Neueste",
"chat_len_over": "Nachricht zu lang",
"chat_locked_hint": "Entsperre deine Wallet, um deine Chats zu laden.",
@@ -157,6 +159,8 @@
"chat_new_title": "Neue Unterhaltung",
"chat_new_zaddr": "z-Adresse des Empfängers",
"chat_no_matches": "Keine Unterhaltungen entsprechen deiner Suche.",
"chat_no_z_contacts": "Noch keine Kontakte mit geschützter Adresse",
"chat_pick_contact": "Aus Kontakten wählen…",
"chat_retry": "Wiederholen",
"chat_search": "Unterhaltungen durchsuchen",
"chat_select_hint": "Wähle eine Unterhaltung aus, um sie anzuzeigen.",

View File

@@ -146,6 +146,8 @@
"chat_export_done": "Conversación exportada",
"chat_export_failed": "No se pudo escribir el archivo de exportación.",
"chat_export_warn": "Guarda los mensajes descifrados como texto sin cifrar. Guarda el archivo de forma segura.",
"chat_hidden_toast": "Conversación oculta: un mensaje nuevo la recupera",
"chat_hide": "Ocultar",
"chat_jump_latest": "Recientes",
"chat_len_over": "Mensaje demasiado largo",
"chat_locked_hint": "Desbloquea tu monedero para cargar tus chats.",
@@ -157,6 +159,8 @@
"chat_new_title": "Nueva conversación",
"chat_new_zaddr": "Dirección z del destinatario",
"chat_no_matches": "Ninguna conversación coincide con tu búsqueda.",
"chat_no_z_contacts": "Aún no hay contactos con dirección blindada",
"chat_pick_contact": "Elegir de contactos…",
"chat_retry": "Reintentar",
"chat_search": "Buscar conversaciones",
"chat_select_hint": "Selecciona una conversación para verla.",

View File

@@ -146,6 +146,8 @@
"chat_export_done": "Conversation exportée",
"chat_export_failed": "Impossible d'écrire le fichier d'exportation.",
"chat_export_warn": "Enregistre les messages déchiffrés en texte clair. Conservez le fichier en lieu sûr.",
"chat_hidden_toast": "Conversation masquée — un nouveau message la fait réapparaître",
"chat_hide": "Masquer",
"chat_jump_latest": "Récents",
"chat_len_over": "Message trop long",
"chat_locked_hint": "Déverrouillez votre portefeuille pour charger vos discussions.",
@@ -157,6 +159,8 @@
"chat_new_title": "Nouvelle conversation",
"chat_new_zaddr": "Adresse Z du destinataire",
"chat_no_matches": "Aucune conversation ne correspond à votre recherche.",
"chat_no_z_contacts": "Aucun contact avec adresse blindée pour l'instant",
"chat_pick_contact": "Choisir dans les contacts…",
"chat_retry": "Réessayer",
"chat_search": "Rechercher des conversations",
"chat_select_hint": "Sélectionnez une conversation pour l'afficher.",

View File

@@ -146,6 +146,8 @@
"chat_export_done": "会話をエクスポートしました",
"chat_export_failed": "エクスポートファイルを書き込めませんでした。",
"chat_export_warn": "復号したメッセージを平文で保存します。ファイルは安全に保管してください。",
"chat_hidden_toast": "会話を非表示にしました — 新しいメッセージが届くと再表示されます",
"chat_hide": "非表示",
"chat_jump_latest": "最新",
"chat_len_over": "メッセージが長すぎます",
"chat_locked_hint": "チャットを読み込むにはウォレットのロックを解除してください。",
@@ -157,6 +159,8 @@
"chat_new_title": "新しい会話",
"chat_new_zaddr": "宛先Zアドレス",
"chat_no_matches": "検索に一致する会話がありません。",
"chat_no_z_contacts": "シールドアドレスの連絡先はまだありません",
"chat_pick_contact": "連絡先から選択…",
"chat_retry": "再送信",
"chat_search": "会話を検索",
"chat_select_hint": "表示する会話を選択してください。",

View File

@@ -146,6 +146,8 @@
"chat_export_done": "대화를 내보냈습니다",
"chat_export_failed": "내보내기 파일을 쓸 수 없습니다.",
"chat_export_warn": "복호화된 메시지를 일반 텍스트로 저장합니다. 파일을 안전하게 보관하세요.",
"chat_hidden_toast": "대화를 숨겼습니다 — 새 메시지가 오면 다시 표시됩니다",
"chat_hide": "숨기기",
"chat_jump_latest": "최신",
"chat_len_over": "메시지가 너무 깁니다",
"chat_locked_hint": "채팅을 불러오려면 지갑 잠금을 해제하세요.",
@@ -157,6 +159,8 @@
"chat_new_title": "새 대화",
"chat_new_zaddr": "받는 사람 z-주소",
"chat_no_matches": "검색과 일치하는 대화가 없습니다.",
"chat_no_z_contacts": "보호 주소 연락처가 아직 없습니다",
"chat_pick_contact": "연락처에서 선택…",
"chat_retry": "다시 시도",
"chat_search": "대화 검색",
"chat_select_hint": "볼 대화를 선택하세요.",

View File

@@ -146,6 +146,8 @@
"chat_export_done": "Conversa exportada",
"chat_export_failed": "Não foi possível gravar o arquivo de exportação.",
"chat_export_warn": "Salva as mensagens descriptografadas como texto simples. Guarde o arquivo com segurança.",
"chat_hidden_toast": "Conversa ocultada — uma nova mensagem a traz de volta",
"chat_hide": "Ocultar",
"chat_jump_latest": "Recentes",
"chat_len_over": "Mensagem muito longa",
"chat_locked_hint": "Desbloqueie sua carteira para carregar suas conversas.",
@@ -157,6 +159,8 @@
"chat_new_title": "Nova conversa",
"chat_new_zaddr": "Endereço-z do destinatário",
"chat_no_matches": "Nenhuma conversa corresponde à sua pesquisa.",
"chat_no_z_contacts": "Ainda não há contatos com endereço blindado",
"chat_pick_contact": "Escolher dos contatos…",
"chat_retry": "Tentar novamente",
"chat_search": "Pesquisar conversas",
"chat_select_hint": "Selecione uma conversa para visualizá-la.",

View File

@@ -146,6 +146,8 @@
"chat_export_done": "Разговор экспортирован",
"chat_export_failed": "Не удалось записать файл экспорта.",
"chat_export_warn": "Сохраняет расшифрованные сообщения в виде обычного текста. Храните файл в надёжном месте.",
"chat_hidden_toast": "Разговор скрыт — новое сообщение вернёт его",
"chat_hide": "Скрыть",
"chat_jump_latest": "Новые",
"chat_len_over": "Сообщение слишком длинное",
"chat_locked_hint": "Разблокируйте кошелёк, чтобы загрузить переписку.",
@@ -157,6 +159,8 @@
"chat_new_title": "Новая переписка",
"chat_new_zaddr": "Z-адрес получателя",
"chat_no_matches": "Нет разговоров, соответствующих запросу.",
"chat_no_z_contacts": "Пока нет контактов с защищённым адресом",
"chat_pick_contact": "Выбрать из контактов…",
"chat_retry": "Повторить",
"chat_search": "Поиск разговоров",
"chat_select_hint": "Выберите переписку для просмотра.",

View File

@@ -146,6 +146,8 @@
"chat_export_done": "会话已导出",
"chat_export_failed": "无法写入导出文件。",
"chat_export_warn": "将解密后的消息保存为纯文本。请妥善保管该文件。",
"chat_hidden_toast": "会话已隐藏——收到新消息后会重新显示",
"chat_hide": "隐藏",
"chat_jump_latest": "最新",
"chat_len_over": "消息过长",
"chat_locked_hint": "解锁钱包以加载您的聊天记录。",
@@ -157,6 +159,8 @@
"chat_new_title": "新建对话",
"chat_new_zaddr": "收款方 z 地址",
"chat_no_matches": "没有与搜索匹配的会话。",
"chat_no_z_contacts": "暂无使用隐私地址的联系人",
"chat_pick_contact": "从联系人中选择…",
"chat_retry": "重试",
"chat_search": "搜索会话",
"chat_select_hint": "选择一个对话以查看。",

View File

@@ -1262,6 +1262,7 @@ int App::chatUnreadCount() const
const auto& store = chat_service_.store();
for (const auto& cid : store.conversationIds()) {
if (settings_ && settings_->isChatMuted(cid)) continue; // muted conversations don't badge (Q10)
if (settings_ && settings_->isChatHidden(cid)) continue; // hidden conversations don't badge
std::int64_t seen = 0;
const auto it = chat_seen_watermark_.find(cid);
if (it != chat_seen_watermark_.end()) seen = it->second;
@@ -1719,6 +1720,13 @@ void App::refreshTransactionData()
for (const auto& tx : result.transactions) chatTxTimes[tx.txid] = tx.timestamp;
std::vector<std::string> newChatCids;
chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr), &newChatCids);
// A new message un-hides a hidden conversation (you can't un-receive), so nothing is lost.
if (settings_) {
bool unhid = false;
for (const auto& cid : newChatCids)
if (settings_->isChatHidden(cid)) { settings_->setChatHidden(cid, false); unhid = true; }
if (unhid) settings_->save();
}
// Toast when a NON-muted conversation received a new message and we're off the Chat tab.
// Gate on the ingest-reported cids, not a seen-watermark delta — the latter can be swallowed
// by block-time vs wall-clock (echo) skew (Q10 + Q4).
@@ -1785,6 +1793,13 @@ void App::refreshRecentTransactionData()
for (const auto& tx : result.transactions) chatTxTimes[tx.txid] = tx.timestamp;
std::vector<std::string> newChatCids;
chat_service_.ingest(result.hushChatMetadata, chatTxTimes, std::time(nullptr), &newChatCids);
// A new message un-hides a hidden conversation (you can't un-receive), so nothing is lost.
if (settings_) {
bool unhid = false;
for (const auto& cid : newChatCids)
if (settings_->isChatHidden(cid)) { settings_->setChatHidden(cid, false); unhid = true; }
if (unhid) settings_->save();
}
// Toast when a NON-muted conversation received a new message and we're off the Chat tab.
// Gate on the ingest-reported cids, not a seen-watermark delta — the latter can be swallowed
// by block-time vs wall-clock (echo) skew (Q10 + Q4).

View File

@@ -152,6 +152,11 @@ bool Settings::load(const std::string& path)
for (const auto& c : j["muted_chat_cids"])
if (c.is_string()) muted_chat_cids_.push_back(c.get<std::string>());
}
if (j.contains("hidden_chat_cids") && j["hidden_chat_cids"].is_array()) {
hidden_chat_cids_.clear();
for (const auto& c : j["hidden_chat_cids"])
if (c.is_string()) hidden_chat_cids_.push_back(c.get<std::string>());
}
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
loadScalar(j, "acrylic_quality", acrylic_quality_);
loadScalar(j, "blur_multiplier", blur_multiplier_);
@@ -430,6 +435,9 @@ bool Settings::save(const std::string& path)
j["muted_chat_cids"] = json::array();
for (const auto& c : muted_chat_cids_)
j["muted_chat_cids"].push_back(c);
j["hidden_chat_cids"] = json::array();
for (const auto& c : hidden_chat_cids_)
j["hidden_chat_cids"].push_back(c);
j["acrylic_enabled"] = acrylic_enabled_;
j["acrylic_quality"] = acrylic_quality_;
j["blur_multiplier"] = blur_multiplier_;

View File

@@ -130,6 +130,19 @@ public:
muted_chat_cids_.end());
}
// Hidden chat conversations (by cid) — hidden ones are filtered out of the list; a new incoming
// message un-hides them (you can't un-receive) so nothing is silently lost.
bool isChatHidden(const std::string& cid) const {
return std::find(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid) != hidden_chat_cids_.end();
}
void setChatHidden(const std::string& cid, bool hidden) {
const bool already = isChatHidden(cid);
if (hidden && !already) hidden_chat_cids_.push_back(cid);
else if (!hidden && already)
hidden_chat_cids_.erase(std::remove(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid),
hidden_chat_cids_.end());
}
// Privacy
bool getSaveZtxs() const { return save_ztxs_; }
void setSaveZtxs(bool save) { save_ztxs_ = save; }
@@ -486,6 +499,7 @@ private:
std::string skin_id_ = "dragonx";
std::string chat_reply_zaddr_;
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
std::vector<std::string> hidden_chat_cids_; // hidden chat conversations by cid
bool save_ztxs_ = true;
bool auto_shield_ = true;
bool use_tor_ = false;

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()));

View File

@@ -252,6 +252,10 @@ void I18n::loadBuiltinEnglish()
strings_["chat_len_over"] = "Message too long";
strings_["chat_mute"] = "Mute";
strings_["chat_unmute"] = "Unmute";
strings_["chat_hide"] = "Hide";
strings_["chat_hidden_toast"] = "Conversation hidden — a new message brings it back";
strings_["chat_pick_contact"] = "Choose from contacts\xE2\x80\xA6";
strings_["chat_no_z_contacts"] = "No shielded-address contacts yet";
// Seed-phrase backup (full-node)
strings_["seed_backup_button"] = "Seed phrase";
strings_["tt_seed_backup"] = "Show and back up your wallet's 24-word recovery seed phrase";