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:
@@ -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