diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index a6d186f..043abbb 100644 Binary files a/res/fonts/NotoSansCJK-Subset.ttf and b/res/fonts/NotoSansCJK-Subset.ttf differ diff --git a/res/lang/de.json b/res/lang/de.json index ac91ca4..9100aed 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -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.", diff --git a/res/lang/es.json b/res/lang/es.json index bd11176..8aeadbe 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -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.", diff --git a/res/lang/fr.json b/res/lang/fr.json index aa54a97..773188a 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -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.", diff --git a/res/lang/ja.json b/res/lang/ja.json index ba02496..5cf26d2 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -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": "表示する会話を選択してください。", diff --git a/res/lang/ko.json b/res/lang/ko.json index a62e0cc..ec3300a 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -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": "볼 대화를 선택하세요.", diff --git a/res/lang/pt.json b/res/lang/pt.json index 84a8bb3..20107c8 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -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.", diff --git a/res/lang/ru.json b/res/lang/ru.json index 5ef3838..a1cc8f8 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -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": "Выберите переписку для просмотра.", diff --git a/res/lang/zh.json b/res/lang/zh.json index 71c2d4f..fff5e90 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -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": "选择一个对话以查看。", diff --git a/src/app_network.cpp b/src/app_network.cpp index e74b8a4..181a89d 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -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 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 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). diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 6e82421..499a742 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -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()); } + 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()); + } 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_; diff --git a/src/config/settings.h b/src/config/settings.h index d698df7..59f31d4 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -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 muted_chat_cids_; // muted chat conversations by cid (Q10) + std::vector hidden_chat_cids_; // hidden chat conversations by cid bool save_ztxs_ = true; bool auto_shield_ = true; bool use_tor_ = false; diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index aa0303b..32c5571 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -216,6 +216,7 @@ void RenderChatTab(App* app) // Build conversation summaries (single scan per conversation), sorted by most-recent activity. std::vector 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())); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index f951f58..e9f1a50 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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";