feat(chat): demo-chat seed for the screenshot sweep (debug)

Add App::seedChatDemoData() + a "Seed demo chat" button in the DEBUG OPTIONS
card (shown only in chat-enabled builds, next to the screenshot sweep). It gives
the Chat tab a demo identity and injects a few sample conversations into the
in-memory store (NOT persisted — no DB attached, gone on restart) so the
screenshot sweep captures the tab's real UI (conversation list, thread bubbles,
composer, and the "waiting for reply" contact-request state) instead of the
empty "unlock" hint. No-op when the chat feature is off.

Verified: Linux + Windows build with chat ON, ctest 100%, hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 19:47:41 -05:00
parent 008cc82ee5
commit 516e5ec688
3 changed files with 50 additions and 0 deletions

View File

@@ -174,6 +174,10 @@ public:
// message (to a conversation whose peer key we know) / a new-conversation contact request. // message (to a conversation whose peer key we know) / a new-conversation contact request.
void sendChatMessage(const std::string& conversationId, const std::string& text); void sendChatMessage(const std::string& conversationId, const std::string& text);
void startChatConversation(const std::string& peerZaddr, const std::string& text); void startChatConversation(const std::string& peerZaddr, const std::string& text);
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
// the chat feature is off.
void seedChatDemoData();
// Reason the lite wallet failed to auto-open this session (empty if none / opened OK). // Reason the lite wallet failed to auto-open this session (empty if none / opened OK).
const std::string& liteOpenError() const { return lite_open_error_; } const std::string& liteOpenError() const { return lite_open_error_; }
// Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet). // Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).

View File

@@ -2660,6 +2660,46 @@ void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model)
if (!metadata.empty()) chat_service_.ingest(metadata, txTimestamps, std::time(nullptr)); if (!metadata.empty()) chat_service_.ingest(metadata, txTimestamps, std::time(nullptr));
} }
void App::seedChatDemoData()
{
if (!chat::hushChatFeatureEnabledAtBuild()) return;
// Demo identity so the tab renders the populated UI (hasIdentity()==true). Injected messages go
// straight into the in-memory store (no DB attached here → not persisted, gone on restart).
chat::ChatKeyPair keys;
if (chat::deriveChatIdentityFromSecret("obsidian-dragon-demo-chat", keys).status
== chat::ChatIdentityStatus::Ready) {
chat_service_.setIdentity(keys);
}
chat::wipeChatKeyPair(keys);
auto& store = chat_service_.store();
const std::string zAlice = "zs1demoalice6xh2n8fchrz23thcgqqd2353v8ev2pr7p7lq4p3elsyrfkuenq";
const std::string zBob = "zs1demobob9k4p3elsyrfkuenq4kl79j2pg7l3h2juz4t6q9wqxkha2n8fchrz2";
auto add = [&](const std::string& cid, const std::string& z, const std::string& pk,
chat::ChatDirection dir, chat::ChatMessageKind kind, const std::string& body,
std::int64_t ts, const std::string& id) {
chat::ChatMessage m;
m.direction = dir; m.kind = kind; m.conversation_id = cid; m.peer_zaddr = z;
m.peer_public_key_hex = pk; m.body = body; m.timestamp = ts; m.txid = id;
m.payload_position = 0;
store.append(m);
};
// A full thread (peer key known → composer enabled).
add("demo-1", zAlice, std::string(64, 'a'), chat::ChatDirection::Incoming, chat::ChatMessageKind::Message,
"hey! got the DragonX wallet running, chat works great", 1751284800, "demo:a1");
add("demo-1", zAlice, std::string(64, 'a'), chat::ChatDirection::Outgoing, chat::ChatMessageKind::Message,
"nice - this is end-to-end encrypted over shielded memos", 1751285400, "demo:a2");
add("demo-1", zAlice, std::string(64, 'a'), chat::ChatDirection::Incoming, chat::ChatMessageKind::Message,
"and it survives the daemon's output shuffle now", 1751286000, "demo:a3");
// An incoming contact request (peer key known → can reply).
add("demo-2", zBob, std::string(64, 'b'), chat::ChatDirection::Incoming, chat::ChatMessageKind::ContactRequest,
"hi, add me? - bob", 1751200000, "demo:b1");
// An outgoing contact request awaiting a reply (no peer key → "waiting" composer state).
add("demo-3", "zs1demopeerawaitingtheirfirstreplybeforewecanmessagethemyet00", "",
chat::ChatDirection::Outgoing, chat::ChatMessageKind::ContactRequest, "hey, let's chat", 1751100000, "demo:c1");
}
void App::exportAllKeys(std::function<void(const std::string&, int, int)> callback) void App::exportAllKeys(std::function<void(const std::string&, int, int)> callback)
{ {
if (!state_.connected || !rpc_) { if (!state_.connected || !rpc_) {

View File

@@ -2496,6 +2496,12 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(); ImGui::SameLine();
if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button"))) if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button")))
util::Platform::openFolder(app->screenshotDir()); util::Platform::openFolder(app->screenshotDir());
if (chat::hushChatFeatureEnabledAtBuild()) {
// Populate the Chat tab with demo conversations so the sweep captures its real UI.
ImGui::SameLine();
if (TactileButton("Seed demo chat", ImVec2(0, 0), S.resolveFont("button")))
app->seedChatDemoData();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Separator(); ImGui::Separator();
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));