feat(chat): unread count + sidebar badge (Q1)

Track a per-conversation "last seen" watermark (message timestamp) on App:
- chatUnreadCount() sums incoming messages newer than each conversation's
  watermark; surfaced as SidebarStatus.chatUnreadCount → a badge on the Chat nav
  item (mirrors the History/Peers badges).
- Viewing a thread marks it seen (markChatConversationSeen while displayed).
- Baseline on load: existing stored messages are marked seen, so only messages
  that arrive while the app is open badge as unread.
- Wiped in resetChatSession so unread state never leaks across a wallet switch.
In-memory only (resets on app restart); persistence is a later refinement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 15:56:58 -05:00
parent 055685a4c4
commit 100ed01469
5 changed files with 44 additions and 0 deletions

View File

@@ -1227,6 +1227,26 @@ void App::processWalletSwitchRevert()
daemon_restarting_ = false; // the connect loop now restarts the previous wallet's daemon
}
int App::chatUnreadCount() const
{
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return 0;
int unread = 0;
const auto& store = chat_service_.store();
for (const auto& cid : store.conversationIds()) {
std::int64_t seen = 0;
const auto it = chat_seen_watermark_.find(cid);
if (it != chat_seen_watermark_.end()) seen = it->second;
for (const auto& m : store.conversation(cid))
if (m.direction == chat::ChatDirection::Incoming && m.timestamp > seen) ++unread;
}
return unread;
}
void App::markChatConversationSeen(const std::string& cid, std::int64_t latestTs)
{
if (latestTs > 0) chat_seen_watermark_[cid] = latestTs;
}
void App::wipePendingTransactionHistoryCachePassphrase()
{
if (!pending_transaction_history_cache_passphrase_.empty()) {
@@ -2658,6 +2678,13 @@ void App::provisionChatIdentityFromSecret(std::string secret)
chat_service_.setPersistence(&chat_db_);
if (chat_db_.unlockWithSecret(trimmed)) {
chat_service_.loadFromDatabase();
// Baseline unread: treat everything already in the store at load as read, so only messages
// that arrive while the app is open badge as unread (Q1).
const auto& store = chat_service_.store();
for (const auto& cid : store.conversationIds()) {
const auto msgs = store.conversation(cid);
if (!msgs.empty()) chat_seen_watermark_[cid] = msgs.back().timestamp;
}
}
} else {
chat_identity_unavailable_ = true;
@@ -2677,6 +2704,7 @@ void App::resetChatSession()
// Drop identity + decrypted plaintext in RAM, lock the seed-encrypted DB, re-arm provisioning.
chat_service_.clearIdentity();
chat_service_.store().clear();
chat_seen_watermark_.clear(); // unread state is per-wallet — don't leak it across a switch
ui::ResetChatTab(); // wipe the chat UI's typed plaintext + selection so it can't leak into the next wallet
chat_db_.lock();
chat_identity_provisioned_ = false;