Files
ObsidianDragon/src/data/address_book.h
T
DanSandClaude Opus 4.8 0a042df8e0 feat(chat): per-conversation delete (revive + block); memoize chat/badge render
Adds a per-conversation "delete" with two modes, and removes the per-frame
rescans of the chat history that shared these files.

Delete conversation (header trash icon → confirm dialog):
- Delete (revive-on-new-message): clears local history and tombstones the
  messages (new chat_deleted table, keyed dedup hashes) so the every-few-
  seconds memo re-scan can't re-import them; a genuinely NEW message (new
  txid) revives the thread.
- Delete & block: removes history WITHOUT a tombstone and records the cid as
  blocked (settings); ChatService::ingest drops that conversation's messages
  — old and future — until unblocked from the "Blocked" manager, which then
  re-imports the conversation from chain.
- Local-only (messages remain on-chain; the peer keeps their copy).
  deleteConversation() deletes the DB rows FIRST and only then mutates the
  store, so a failed write can't leave the two diverged.
Unit-tested (revive / tombstone-survives-reload / block / unblock) and
adversarially reviewed (store/DB divergence, half-open DB, revive-unread).

Performance (chat + badge hot paths, from the perf audit):
- ChatStore gains revision(); the Chat unread badge is now a single O(N)
  no-alloc pass cached on it (was O(conversations x messages) copy+sort every
  frame). The conversation list and open thread are memoized on revision()
  (+ show-hidden and AddressBook::revision() for peer-name resolution).
- AddressBook gains revision() so an in-place contact rename invalidates the
  chat memo (an edit keeps entries().size() constant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 22:54:51 -05:00

151 lines
4.8 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace dragonx {
namespace data {
/**
* @brief A single address book entry
*/
struct AddressBookEntry {
std::string label;
std::string address;
std::string notes;
// Per-wallet visibility scope: "global" (shown in every wallet) or a wallet-identity hash
// (shown only when that wallet is the active one). Empty is treated as "global" so legacy
// entries — written before multi-wallet scoping — keep showing everywhere.
std::string scope = "global";
// Contact avatar: "" = default type badge (Z/T), "icon:<name>" = a Material wallet-icon,
// "img:<path>" = a custom image (copied into <config>/contact-avatars/).
std::string avatar;
AddressBookEntry() = default;
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "",
const std::string& s = "global")
: label(l), address(a), notes(n), scope(s) {}
bool isGlobal() const { return scope.empty() || scope == "global"; }
// Visible under the wallet whose identity hash is walletHash (or globally).
bool visibleInWallet(const std::string& walletHash) const {
return isGlobal() || scope == walletHash;
}
};
/**
* @brief Address book manager
*
* Stores labeled addresses for easy access when sending.
*/
class AddressBook {
public:
AddressBook();
~AddressBook();
/**
* @brief Load address book from disk
* @return true if loaded successfully
*/
bool load();
/**
* @brief Save address book to disk
* @return true if saved successfully
*/
bool save();
/**
* @brief Get default address book file path
*/
static std::string getDefaultPath();
/**
* @brief Add a new entry
* @param entry The entry to add
* @return true if added (false if duplicate address)
*/
bool addEntry(const AddressBookEntry& entry);
/**
* @brief Update an existing entry
* @param index Index of entry to update
* @param entry New entry data
* @return true if updated
*/
bool updateEntry(size_t index, const AddressBookEntry& entry);
/**
* @brief Remove an entry
* @param index Index of entry to remove
* @return true if removed
*/
bool removeEntry(size_t index);
/**
* @brief Re-attach contacts stuck on a legacy (drifting address-hash) scope to a stable wallet id.
* Rewrites every non-global entry whose scope is NOT already a stable "w:"-prefixed id to
* `scopeId`, and saves once if anything changed. Recovery for contacts orphaned when the
* old address-set-hash scope shifted (e.g. after creating a new address).
* @return number of entries re-scoped.
*/
int reattachLegacyScopes(const std::string& scopeId);
/**
* @brief Find entry by address (any scope). Used for contact-label lookups.
* @param address Address to search for
* @return Index or -1 if not found
*/
int findByAddress(const std::string& address) const;
/**
* @brief Is there an existing entry with this address that would be visible ALONGSIDE a new
* entry of the given scope? (Same wallet, or either side global.) Used to reject
* duplicates within a wallet while still allowing the same address in different wallets.
* @param excludeIndex Storage index to skip (for edit), or -1.
*/
bool hasVisibleDuplicate(const std::string& address, const std::string& scope,
int excludeIndex = -1) const;
/**
* @brief Get all entries
*/
const std::vector<AddressBookEntry>& entries() const { return entries_; }
/**
* @brief Get entry count
*/
size_t size() const { return entries_.size(); }
/**
* @brief Monotonic counter bumped on every content change (add/update/remove/load/sweep). Consumers
* (e.g. the chat conversation-list memo) key their caches off this so an IN-PLACE edit — a rename or
* address change that keeps size() constant — still invalidates them.
*/
std::uint64_t revision() const { return revision_; }
/**
* @brief UI-sweep ONLY: replace the in-memory entries WITHOUT persisting to disk, so the sweep can
* seed demo contacts and restore the real book without a disk write. Do not use outside the sweep.
*/
void sweepSetEntries(std::vector<AddressBookEntry> e) { entries_ = std::move(e); ++revision_; }
/**
* @brief Check if empty
*/
bool empty() const { return entries_.empty(); }
private:
std::vector<AddressBookEntry> entries_;
std::uint64_t revision_ = 0;
std::string file_path_;
};
} // namespace data
} // namespace dragonx