feat(chat): read-only Chat sidebar tab (Phase 3)
Add the first user-visible HushChat surface: a Chat tab in the sidebar with a
two-pane, read-only conversation view — a conversation list on the left and the
selected thread on the right — reading from the App-owned ChatService store.
- New src/ui/windows/chat_tab.{h,cpp} (RenderChatTab(App*), mirroring the
Contacts tab). Conversations are sorted by most-recent activity; peer
z-addresses resolve to contact names via the address book when known; each
message shows sender + timestamp + wrapped body, with a contact-request tag
and a read-only footer (composing arrives in a later phase). Empty states
cover "wallet locked / identity not ready" and "no conversations yet".
- Sidebar wiring: NavPage::Chat + registry entry (static_assert stays balanced),
NavPageSurface + GetNavIconMD (ICON_MD_CHAT), dispatch in app.cpp, trace/sweep
page names, and App::chatService() accessor. The tab is gated on the new
WalletUiSurface::Chat, which returns DRAGONX_ENABLE_CHAT != 0 — so it only
appears in chat-enabled builds and is hidden (and unreachable) by default.
- i18n: English defaults for the chat nav label + hint strings.
Verified: Linux + Windows(mingw) build with chat ON, ctest 100%, hygiene clean;
caches restored to the OFF default. The screenshot sweep now includes the Chat
tab (sweepPageName "chat") for per-theme visual review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -476,6 +476,7 @@ set(APP_SOURCES
|
|||||||
src/ui/windows/qr_popup_dialog.cpp
|
src/ui/windows/qr_popup_dialog.cpp
|
||||||
src/ui/windows/validate_address_dialog.cpp
|
src/ui/windows/validate_address_dialog.cpp
|
||||||
src/ui/windows/contacts_tab.cpp
|
src/ui/windows/contacts_tab.cpp
|
||||||
|
src/ui/windows/chat_tab.cpp
|
||||||
src/ui/windows/shield_dialog.cpp
|
src/ui/windows/shield_dialog.cpp
|
||||||
src/ui/windows/request_payment_dialog.cpp
|
src/ui/windows/request_payment_dialog.cpp
|
||||||
src/ui/windows/block_info_dialog.cpp
|
src/ui/windows/block_info_dialog.cpp
|
||||||
@@ -606,6 +607,7 @@ set(APP_HEADERS
|
|||||||
src/ui/windows/qr_popup_dialog.h
|
src/ui/windows/qr_popup_dialog.h
|
||||||
src/ui/windows/validate_address_dialog.h
|
src/ui/windows/validate_address_dialog.h
|
||||||
src/ui/windows/contacts_tab.h
|
src/ui/windows/contacts_tab.h
|
||||||
|
src/ui/windows/chat_tab.h
|
||||||
src/ui/windows/contact_picker.h
|
src/ui/windows/contact_picker.h
|
||||||
src/ui/windows/shield_dialog.h
|
src/ui/windows/shield_dialog.h
|
||||||
src/ui/windows/request_payment_dialog.h
|
src/ui/windows/request_payment_dialog.h
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
#include "ui/windows/qr_popup_dialog.h"
|
#include "ui/windows/qr_popup_dialog.h"
|
||||||
#include "ui/windows/validate_address_dialog.h"
|
#include "ui/windows/validate_address_dialog.h"
|
||||||
#include "ui/windows/contacts_tab.h"
|
#include "ui/windows/contacts_tab.h"
|
||||||
|
#include "ui/windows/chat_tab.h"
|
||||||
#include "ui/windows/shield_dialog.h"
|
#include "ui/windows/shield_dialog.h"
|
||||||
#include "ui/windows/request_payment_dialog.h"
|
#include "ui/windows/request_payment_dialog.h"
|
||||||
#include "ui/windows/block_info_dialog.h"
|
#include "ui/windows/block_info_dialog.h"
|
||||||
@@ -1709,6 +1710,9 @@ void App::render()
|
|||||||
case ui::NavPage::Contacts:
|
case ui::NavPage::Contacts:
|
||||||
ui::RenderContactsTab(this);
|
ui::RenderContactsTab(this);
|
||||||
break;
|
break;
|
||||||
|
case ui::NavPage::Chat:
|
||||||
|
ui::RenderChatTab(this);
|
||||||
|
break;
|
||||||
case ui::NavPage::Mining:
|
case ui::NavPage::Mining:
|
||||||
ui::RenderMiningTab(this);
|
ui::RenderMiningTab(this);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ public:
|
|||||||
config::Settings* settings() { return settings_.get(); }
|
config::Settings* settings() { return settings_.get(); }
|
||||||
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
||||||
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
||||||
|
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
|
||||||
|
chat::ChatService& chatService() { return chat_service_; }
|
||||||
// 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).
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ const char* tracePageName(ui::NavPage page)
|
|||||||
case ui::NavPage::Receive: return "Receive tab";
|
case ui::NavPage::Receive: return "Receive tab";
|
||||||
case ui::NavPage::History: return "History tab";
|
case ui::NavPage::History: return "History tab";
|
||||||
case ui::NavPage::Contacts: return "Contacts tab";
|
case ui::NavPage::Contacts: return "Contacts tab";
|
||||||
|
case ui::NavPage::Chat: return "Chat tab";
|
||||||
case ui::NavPage::Mining: return "Mining tab";
|
case ui::NavPage::Mining: return "Mining tab";
|
||||||
case ui::NavPage::Market: return "Market tab";
|
case ui::NavPage::Market: return "Market tab";
|
||||||
case ui::NavPage::Console: return "Console tab";
|
case ui::NavPage::Console: return "Console tab";
|
||||||
@@ -761,6 +762,7 @@ static const char* sweepPageName(ui::NavPage page)
|
|||||||
case ui::NavPage::Receive: return "receive";
|
case ui::NavPage::Receive: return "receive";
|
||||||
case ui::NavPage::History: return "history";
|
case ui::NavPage::History: return "history";
|
||||||
case ui::NavPage::Contacts: return "contacts";
|
case ui::NavPage::Contacts: return "contacts";
|
||||||
|
case ui::NavPage::Chat: return "chat";
|
||||||
case ui::NavPage::Mining: return "mining";
|
case ui::NavPage::Mining: return "mining";
|
||||||
case ui::NavPage::Market: return "market";
|
case ui::NavPage::Market: return "market";
|
||||||
case ui::NavPage::Console: return "console";
|
case ui::NavPage::Console: return "console";
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ enum class NavPage {
|
|||||||
Receive,
|
Receive,
|
||||||
History,
|
History,
|
||||||
Contacts,
|
Contacts,
|
||||||
|
Chat,
|
||||||
// --- separator ---
|
// --- separator ---
|
||||||
Mining,
|
Mining,
|
||||||
Market,
|
Market,
|
||||||
@@ -53,6 +54,7 @@ inline const NavItem kNavItems[] = {
|
|||||||
{ "Receive", NavPage::Receive, nullptr, "receive", nullptr },
|
{ "Receive", NavPage::Receive, nullptr, "receive", nullptr },
|
||||||
{ "History", NavPage::History, nullptr, "history", nullptr },
|
{ "History", NavPage::History, nullptr, "history", nullptr },
|
||||||
{ "Contacts", NavPage::Contacts, nullptr, "contacts", nullptr },
|
{ "Contacts", NavPage::Contacts, nullptr, "contacts", nullptr },
|
||||||
|
{ "Chat", NavPage::Chat, nullptr, "chat", nullptr },
|
||||||
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
||||||
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
||||||
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
||||||
@@ -81,6 +83,7 @@ inline wallet::WalletUiSurface NavPageSurface(NavPage page)
|
|||||||
case NavPage::Receive: return wallet::WalletUiSurface::Receive;
|
case NavPage::Receive: return wallet::WalletUiSurface::Receive;
|
||||||
case NavPage::History: return wallet::WalletUiSurface::History;
|
case NavPage::History: return wallet::WalletUiSurface::History;
|
||||||
case NavPage::Contacts: return wallet::WalletUiSurface::Contacts;
|
case NavPage::Contacts: return wallet::WalletUiSurface::Contacts;
|
||||||
|
case NavPage::Chat: return wallet::WalletUiSurface::Chat;
|
||||||
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
|
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
|
||||||
case NavPage::Market: return wallet::WalletUiSurface::Market;
|
case NavPage::Market: return wallet::WalletUiSurface::Market;
|
||||||
case NavPage::Console: return wallet::WalletUiSurface::Console;
|
case NavPage::Console: return wallet::WalletUiSurface::Console;
|
||||||
@@ -107,6 +110,7 @@ inline const char* GetNavIconMD(NavPage page)
|
|||||||
case NavPage::Receive: return ICON_MD_CALL_RECEIVED;
|
case NavPage::Receive: return ICON_MD_CALL_RECEIVED;
|
||||||
case NavPage::History: return ICON_MD_HISTORY;
|
case NavPage::History: return ICON_MD_HISTORY;
|
||||||
case NavPage::Contacts: return ICON_MD_CONTACTS;
|
case NavPage::Contacts: return ICON_MD_CONTACTS;
|
||||||
|
case NavPage::Chat: return ICON_MD_CHAT;
|
||||||
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
||||||
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
||||||
case NavPage::Console: return ICON_MD_TERMINAL;
|
case NavPage::Console: return ICON_MD_TERMINAL;
|
||||||
|
|||||||
246
src/ui/windows/chat_tab.cpp
Normal file
246
src/ui/windows/chat_tab.cpp
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// chat_tab.cpp — read-only HushChat view (Phase 3): a conversation list + the
|
||||||
|
// selected thread, both read from the App-owned ChatService store.
|
||||||
|
|
||||||
|
#include "chat_tab.h"
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../data/address_book.h"
|
||||||
|
#include "../../chat/chat_service.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cfloat>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Selected conversation id (cid). Empty => none selected / auto-select first.
|
||||||
|
std::string s_selected_cid;
|
||||||
|
std::string s_scroll_to_cid; // when set, scroll the thread to the bottom next frame
|
||||||
|
|
||||||
|
// Effective draw-list font size for a material font (mirrors sidebar's ScaledFontSize).
|
||||||
|
float scaledSize(ImFont* f) { return f->LegacySize * ImGui::GetStyle().FontScaleMain; }
|
||||||
|
|
||||||
|
std::string shorten(const std::string& s, std::size_t head = 12, std::size_t tail = 6) {
|
||||||
|
if (s.size() <= head + tail + 3) return s;
|
||||||
|
return s.substr(0, head) + "..." + s.substr(s.size() - tail);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string formatTime(std::int64_t ts) {
|
||||||
|
if (ts <= 0) return "";
|
||||||
|
std::time_t t = static_cast<std::time_t>(ts);
|
||||||
|
std::tm* tm = std::localtime(&t); // UI thread only
|
||||||
|
if (!tm) return "";
|
||||||
|
char buf[32];
|
||||||
|
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", tm);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One line of collapsed body text for the list preview (newlines flattened).
|
||||||
|
std::string previewOf(const std::string& body) {
|
||||||
|
std::string out = body;
|
||||||
|
std::replace(out.begin(), out.end(), '\n', ' ');
|
||||||
|
std::replace(out.begin(), out.end(), '\r', ' ');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConvSummary {
|
||||||
|
std::string cid;
|
||||||
|
std::string peerZaddr;
|
||||||
|
std::string peerName; // contact label if known, else a shortened z-addr / cid
|
||||||
|
std::string lastBody;
|
||||||
|
std::int64_t lastTs = 0;
|
||||||
|
int count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Centered, muted, wrapped hint for the empty states.
|
||||||
|
void centeredHint(const char* text) {
|
||||||
|
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||||
|
ImGui::PushFont(material::Type().body2());
|
||||||
|
const float wrap = std::min(avail.x - 40.0f, 420.0f);
|
||||||
|
const ImVec2 sz = ImGui::CalcTextSize(text, nullptr, false, wrap);
|
||||||
|
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + std::max(0.0f, (avail.x - sz.x) * 0.5f),
|
||||||
|
ImGui::GetCursorPos().y + std::max(0.0f, (avail.y - sz.y) * 0.5f)));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap);
|
||||||
|
ImGui::TextUnformatted(text);
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopFont();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void RenderChatTab(App* app)
|
||||||
|
{
|
||||||
|
auto& service = app->chatService();
|
||||||
|
const auto& store = service.store();
|
||||||
|
auto& book = app->addressBook();
|
||||||
|
|
||||||
|
// Not unlocked / identity not derived yet → nothing to show.
|
||||||
|
if (!service.hasIdentity()) {
|
||||||
|
centeredHint(TR("chat_locked_hint"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build conversation summaries, sorted by most-recent activity.
|
||||||
|
std::vector<ConvSummary> convs;
|
||||||
|
for (const auto& cid : store.conversationIds()) {
|
||||||
|
const auto messages = store.conversation(cid);
|
||||||
|
if (messages.empty()) continue;
|
||||||
|
const auto& last = messages.back();
|
||||||
|
ConvSummary c;
|
||||||
|
c.cid = cid;
|
||||||
|
c.peerZaddr = last.peer_zaddr;
|
||||||
|
c.lastBody = last.body;
|
||||||
|
c.lastTs = last.timestamp;
|
||||||
|
c.count = static_cast<int>(messages.size());
|
||||||
|
const int idx = c.peerZaddr.empty() ? -1 : book.findByAddress(c.peerZaddr);
|
||||||
|
c.peerName = (idx >= 0) ? book.entries()[idx].label
|
||||||
|
: shorten(!c.peerZaddr.empty() ? c.peerZaddr : cid);
|
||||||
|
convs.push_back(std::move(c));
|
||||||
|
}
|
||||||
|
std::sort(convs.begin(), convs.end(),
|
||||||
|
[](const ConvSummary& a, const ConvSummary& b) { return a.lastTs > b.lastTs; });
|
||||||
|
|
||||||
|
if (convs.empty()) {
|
||||||
|
centeredHint(TR("chat_empty_hint"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the selection valid; default to the most recent conversation.
|
||||||
|
if (std::none_of(convs.begin(), convs.end(),
|
||||||
|
[](const ConvSummary& c) { return c.cid == s_selected_cid; })) {
|
||||||
|
s_selected_cid = convs.front().cid;
|
||||||
|
s_scroll_to_cid = s_selected_cid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||||
|
const float listW = std::clamp(avail.x * 0.32f, 220.0f, 360.0f);
|
||||||
|
const float pad = 10.0f;
|
||||||
|
const float rowH = 52.0f;
|
||||||
|
|
||||||
|
ImFont* nameFont = material::Type().body2();
|
||||||
|
ImFont* metaFont = material::Type().caption();
|
||||||
|
const float nameSz = scaledSize(nameFont);
|
||||||
|
const float metaSz = scaledSize(metaFont);
|
||||||
|
|
||||||
|
// ---- Left: conversation list ----
|
||||||
|
ImGui::BeginChild("##ChatList", ImVec2(listW, avail.y), true);
|
||||||
|
{
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList(); // the list child's draw list (correct clip/z-order)
|
||||||
|
const ImU32 selBg = ImGui::GetColorU32(ImGuiCol_Header);
|
||||||
|
const ImU32 hoverBg = ImGui::GetColorU32(ImGuiCol_HeaderHovered);
|
||||||
|
for (std::size_t i = 0; i < convs.size(); ++i) {
|
||||||
|
const ConvSummary& c = convs[i];
|
||||||
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||||
|
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||||
|
const bool clicked = ImGui::InvisibleButton("##row", ImVec2(rowW, rowH));
|
||||||
|
const bool hovered = ImGui::IsItemHovered();
|
||||||
|
const bool selected = (c.cid == s_selected_cid);
|
||||||
|
if (clicked) { s_selected_cid = c.cid; s_scroll_to_cid = c.cid; }
|
||||||
|
if (selected || hovered)
|
||||||
|
dl->AddRectFilled(p, ImVec2(p.x + rowW, p.y + rowH), selected ? selBg : hoverBg, 6.0f);
|
||||||
|
|
||||||
|
// Name (top-left).
|
||||||
|
dl->AddText(nameFont, nameSz, ImVec2(p.x + pad, p.y + pad), material::OnSurface(),
|
||||||
|
c.peerName.c_str());
|
||||||
|
// Time (top-right, muted).
|
||||||
|
const std::string when = formatTime(c.lastTs);
|
||||||
|
if (!when.empty()) {
|
||||||
|
const ImVec2 wsz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, when.c_str());
|
||||||
|
dl->AddText(metaFont, metaSz, ImVec2(p.x + rowW - pad - wsz.x, p.y + pad + 1.0f),
|
||||||
|
material::OnSurfaceMedium(), when.c_str());
|
||||||
|
}
|
||||||
|
// Preview (bottom, clipped, muted).
|
||||||
|
const std::string preview = previewOf(c.lastBody);
|
||||||
|
dl->PushClipRect(p, ImVec2(p.x + rowW - pad, p.y + rowH), true);
|
||||||
|
dl->AddText(metaFont, metaSz, ImVec2(p.x + pad, p.y + rowH - pad - metaSz),
|
||||||
|
material::OnSurfaceMedium(), preview.c_str());
|
||||||
|
dl->PopClipRect();
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
// ---- Right: selected conversation thread ----
|
||||||
|
ImGui::BeginChild("##ChatThread", ImVec2(0, avail.y), true);
|
||||||
|
{
|
||||||
|
const ConvSummary* sel = nullptr;
|
||||||
|
for (const auto& c : convs) if (c.cid == s_selected_cid) { sel = &c; break; }
|
||||||
|
|
||||||
|
if (sel) {
|
||||||
|
// Header: peer name + z-address.
|
||||||
|
ImGui::PushFont(material::Type().subtitle1());
|
||||||
|
ImGui::TextUnformatted(sel->peerName.c_str());
|
||||||
|
ImGui::PopFont();
|
||||||
|
if (!sel->peerZaddr.empty()) {
|
||||||
|
ImGui::PushFont(metaFont);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::TextUnformatted(shorten(sel->peerZaddr, 20, 12).c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopFont();
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
const float footerH = ImGui::GetTextLineHeightWithSpacing() + 12.0f;
|
||||||
|
ImGui::BeginChild("##ChatMessages", ImVec2(0, ImGui::GetContentRegionAvail().y - footerH), false);
|
||||||
|
{
|
||||||
|
const auto messages = store.conversation(s_selected_cid);
|
||||||
|
for (const auto& m : messages) {
|
||||||
|
const bool outgoing = (m.direction == chat::ChatDirection::Outgoing);
|
||||||
|
const bool request = (m.kind == chat::ChatMessageKind::ContactRequest);
|
||||||
|
// Meta line: who + time (+ request tag).
|
||||||
|
std::string who = outgoing ? std::string(TR("chat_you")) : sel->peerName;
|
||||||
|
const std::string when = formatTime(m.timestamp);
|
||||||
|
if (!when.empty()) who += " " + when;
|
||||||
|
if (request) who += " [" + std::string(TR("chat_contact_request")) + "]";
|
||||||
|
ImGui::PushFont(metaFont);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||||
|
outgoing ? material::Primary() : material::OnSurfaceMedium());
|
||||||
|
ImGui::TextUnformatted(who.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopFont();
|
||||||
|
// Body (wrapped).
|
||||||
|
ImGui::PushFont(nameFont);
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::TextWrapped("%s", m.body.c_str());
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::Dummy(ImVec2(0.0f, 6.0f));
|
||||||
|
}
|
||||||
|
if (s_scroll_to_cid == s_selected_cid) {
|
||||||
|
ImGui::SetScrollHereY(1.0f);
|
||||||
|
s_scroll_to_cid.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
// Read-only footer: composing arrives in a later phase.
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::PushFont(metaFont);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::TextUnformatted(TR("chat_readonly_note"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopFont();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
27
src/ui/windows/chat_tab.h
Normal file
27
src/ui/windows/chat_tab.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
|
||||||
|
class App;
|
||||||
|
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Render the Chat tab (read-only HushChat conversation view — Phase 3).
|
||||||
|
*
|
||||||
|
* A two-pane view: a conversation list on the left and the selected thread on
|
||||||
|
* the right, both read from the App-owned ChatService store (App::chatService()).
|
||||||
|
* Peer z-addresses are resolved to contact names via the address book when known.
|
||||||
|
* Read-only for now — composing/sending arrives in a later phase. The tab is only
|
||||||
|
* reachable when built with DRAGONX_ENABLE_CHAT (gated via WalletUiSurface::Chat).
|
||||||
|
*
|
||||||
|
* @param app Pointer to the app instance.
|
||||||
|
*/
|
||||||
|
void RenderChatTab(App* app);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -211,6 +211,12 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["transactions"] = "Transactions";
|
strings_["transactions"] = "Transactions";
|
||||||
strings_["history"] = "History";
|
strings_["history"] = "History";
|
||||||
strings_["contacts"] = "Contacts";
|
strings_["contacts"] = "Contacts";
|
||||||
|
strings_["chat"] = "Chat";
|
||||||
|
strings_["chat_locked_hint"] = "Unlock your wallet to load your chats.";
|
||||||
|
strings_["chat_empty_hint"] = "No conversations yet. Messages you receive will appear here.";
|
||||||
|
strings_["chat_you"] = "You";
|
||||||
|
strings_["chat_contact_request"] = "contact request";
|
||||||
|
strings_["chat_readonly_note"] = "Read-only — sending messages arrives in a later update.";
|
||||||
strings_["contacts_search_placeholder"] = "Search contacts...";
|
strings_["contacts_search_placeholder"] = "Search contacts...";
|
||||||
strings_["contacts_search_no_match"] = "No matching contacts";
|
strings_["contacts_search_no_match"] = "No matching contacts";
|
||||||
strings_["address_book_confirm_delete"] = "Confirm delete?";
|
strings_["address_book_confirm_delete"] = "Confirm delete?";
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
#define DRAGONX_ENABLE_LITE_BACKEND 0
|
#define DRAGONX_ENABLE_LITE_BACKEND 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef DRAGONX_ENABLE_CHAT
|
||||||
|
#define DRAGONX_ENABLE_CHAT 0
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace wallet {
|
namespace wallet {
|
||||||
|
|
||||||
@@ -35,6 +39,7 @@ enum class WalletUiSurface {
|
|||||||
Receive,
|
Receive,
|
||||||
History,
|
History,
|
||||||
Contacts, // address book / chat roster; available in both variants (default: return true)
|
Contacts, // address book / chat roster; available in both variants (default: return true)
|
||||||
|
Chat, // HushChat conversations; gated on DRAGONX_ENABLE_CHAT (default OFF)
|
||||||
Mining,
|
Mining,
|
||||||
Market,
|
Market,
|
||||||
Console,
|
Console,
|
||||||
@@ -159,6 +164,8 @@ constexpr bool isUiSurfaceAvailable(const WalletCapabilities& capabilities,
|
|||||||
case WalletUiSurface::LiteNetwork:
|
case WalletUiSurface::LiteNetwork:
|
||||||
case WalletUiSurface::LiteConsole:
|
case WalletUiSurface::LiteConsole:
|
||||||
return !capabilities.fullNodePagesAvailable; // lite builds only
|
return !capabilities.fullNodePagesAvailable; // lite builds only
|
||||||
|
case WalletUiSurface::Chat:
|
||||||
|
return DRAGONX_ENABLE_CHAT != 0; // experimental; compiled-in only when the feature is on
|
||||||
case WalletUiSurface::BootstrapDownload:
|
case WalletUiSurface::BootstrapDownload:
|
||||||
case WalletUiSurface::SetupWizard:
|
case WalletUiSurface::SetupWizard:
|
||||||
case WalletUiSurface::NodeSettings:
|
case WalletUiSurface::NodeSettings:
|
||||||
|
|||||||
Reference in New Issue
Block a user