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:
Binary file not shown.
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "表示する会話を選択してください。",
|
||||
|
||||
@@ -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": "볼 대화를 선택하세요.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "Выберите переписку для просмотра.",
|
||||
|
||||
@@ -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": "选择一个对话以查看。",
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user