2 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 408393a547 feat(chat): add a "Copy my chat address" button
The conversation header only copies a PEER's address, so a user had no way to
hand out their own chat address for others to message them. Add a "Copy my chat
address" link in the left pane (public App::chatMyAddress() forwards the stable
seed-derived reply z-address; copied on click only, with an "address copied"
toast and a warning if none exists yet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 09:45:03 -05:00
DanSandClaude Opus 4.8 a80be443d1 feat(market): flag a stale price instead of showing it as a fresh green number
The aggregate price is deliberately kept on a failed or old refresh (it's never
cleared), so the hero number could be hours out of date while still drawn in the
"live" green. It now turns amber with an "as of N min/h ago" caption and a hover
tooltip carrying the friendly fetch error, when the last refresh failed or the
aggregate is >=5 min old (only when the hero is the aggregate, not an exchange
pair with its own feed). The hardcoded "Price fetch failed" default is now i18n.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 09:42:34 -05:00
5 changed files with 66 additions and 2 deletions
+3
View File
@@ -302,6 +302,9 @@ public:
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_; }
// Public forwarder for the UI: the wallet z-address others use to reach this user in chat. Call on
// click (not per-frame) — the underlying chatReplyZaddr() may persist a freshly chosen address.
std::string chatMyAddress() { return chatReplyZaddr(); }
// Why the Chat tab has no identity yet, so it can show the right prompt instead of always
// "unlock your wallet". Locked = encrypted & locked (seed unreadable); Preparing = unlocked and
// deriving the identity (fetch in flight, or waiting on the node / address list); Unavailable =
+2 -1
View File
@@ -2,6 +2,7 @@
#include "history_scan_policy.h"
#include "rpc/rpc_busy_gate.h"
#include "../util/logger.h"
#include "../util/i18n.h" // util::tr for the price-fetch-failed default
#include <algorithm>
#include <cctype>
@@ -1995,7 +1996,7 @@ void NetworkRefreshService::applyPriceRefreshFailure(WalletState& state,
{
state.market.price_loading = false;
state.market.price_error = errorMessage.empty()
? std::string("Price fetch failed")
? std::string(util::tr("market_price_fetch_failed"))
: errorMessage;
}
+18
View File
@@ -916,6 +916,24 @@ void RenderChatTab(App* app)
if (ImGui::SmallButton(bl.c_str())) s_show_blocked = true;
ImGui::PopStyleColor();
}
// "Copy my chat address" — the address others use to reach this user. The conversation header
// only copies a PEER's address, so without this there's no way to hand out your own. Copies on
// click only (chatMyAddress may persist a freshly chosen address).
{
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
// Text-only: SmallButton uses the body font, which has no Material icon glyph (would show "?").
if (ImGui::SmallButton(TR("chat_copy_my_address"))) {
std::string myAddr = app->chatMyAddress();
if (myAddr.empty()) {
Notifications::instance().warning(TR("chat_no_my_address"));
} else {
ImGui::SetClipboardText(myAddr.c_str());
Notifications::instance().info(TR("address_copied"));
}
}
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("chat_my_address_tip"));
}
const std::string search = s_search;
ImGui::Separator();
if (convs.empty()) {
+36 -1
View File
@@ -31,6 +31,7 @@
#include <algorithm>
#include <cmath>
#include <ctime>
#include <chrono>
#include <unordered_set>
namespace dragonx {
@@ -1496,7 +1497,18 @@ static void mktDrawPriceHero(const MktCtx& cx)
// ---- HERO PRICE (large, prominent) ----
ImFont* h3 = Type().h3();
std::string priceStr = FormatPrice(heroPrice);
ImU32 priceCol = Success();
// Stale-price cue: the aggregate price is kept on a failed/old refresh (it's never cleared), so a
// green number can be hours out of date. Flag it amber when the last refresh failed or the
// aggregate is >=5 min old. Only when the hero IS the aggregate (an exchange pair has its own feed).
const bool haveFetch = market.last_fetch_time.time_since_epoch().count() != 0;
const long ageMin = haveFetch
? (long)std::chrono::duration_cast<std::chrono::minutes>(
std::chrono::steady_clock::now() - market.last_fetch_time).count()
: 0;
const bool usingAggregate = (heroPrice == market.price_usd);
const bool priceStale = usingAggregate && haveFetch &&
(!market.price_error.empty() || ageMin >= 5);
ImU32 priceCol = priceStale ? Warning() : Success();
DrawTextShadow(dl, h3, h3->LegacySize, ImVec2(cx0, cy), priceCol, priceStr.c_str());
// Ticker label after price
@@ -1519,6 +1531,29 @@ static void mktDrawPriceHero(const MktCtx& cx)
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf);
// Stale "as of Nh ago" caption + hover tooltip, right of the change badge (cursor saved/restored
// so the custom draw-list layout below is undisturbed).
if (priceStale) {
char age[64];
if (ageMin >= 60) snprintf(age, sizeof(age), TR("market_price_stale_hour"), (int)(ageMin / 60));
else snprintf(age, sizeof(age), TR("market_price_stale_min"), (int)std::max(1L, ageMin));
ImVec2 asz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, age);
float ax = bMax.x + Layout::spacingMd();
float limit = cardMax.x - pad - 260.0f * dp; // reserve the trade pill + chart toggles
if (ax + asz.x < limit) {
dl->AddText(capFont, capFont->LegacySize, ImVec2(ax, bMin.y + badgePadV), Warning(), age);
ImVec2 sv = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(ax, bMin.y));
ImGui::InvisibleButton("##PriceStale", ImVec2(asz.x, bMax.y - bMin.y));
if (ImGui::IsItemHovered()) {
std::string tip = market.price_error.empty()
? std::string(TR("market_price_fetch_failed")) : market.price_error;
material::Tooltip(TR("market_price_stale_tip"), tip.c_str());
}
ImGui::SetCursorScreenPos(sv);
}
}
// (Divider is drawn in the chart block, below the interval buttons.)
// ---- TRADE BUTTON (top-right of card) ----
+7
View File
@@ -263,6 +263,9 @@ void I18n::loadBuiltinEnglish()
strings_["chat_jump_latest"] = "Latest";
strings_["chat_empty_title"] = "No conversations yet";
strings_["chat_empty_start"] = "Start one with \"New conversation\".";
strings_["chat_copy_my_address"] = "Copy my chat address";
strings_["chat_my_address_tip"] = "Share this address so others can message you";
strings_["chat_no_my_address"] = "No chat address yet";
strings_["chat_search"] = "Search conversations";
strings_["chat_no_matches"] = "No conversations match your search.";
strings_["chat_export"] = "Export chat\xE2\x80\xA6";
@@ -2222,6 +2225,10 @@ void I18n::loadBuiltinEnglish()
strings_["market_24h_change"] = "24h";
strings_["market_price_loading"] = "Loading price data...";
strings_["market_price_unavailable"] = "Price data unavailable";
strings_["market_price_fetch_failed"] = "Price fetch failed";
strings_["market_price_stale_min"] = "as of %d min ago";
strings_["market_price_stale_hour"] = "as of %dh ago";
strings_["market_price_stale_tip"] = "Showing the last known price. The latest refresh failed: %s";
strings_["market_refresh_price"] = "Refresh price data";
strings_["market_trade_on"] = "Trade on %s";