feat(ux): clearer chat delivery, explorer empty/range states, honest settings label
Three next-batch UX-audit fixes:
- Chat delivery: the sending/sent glyph has higher contrast and a tooltip
("Sent to the network — arrives after it confirms", so a check isn't mistaken
for "delivered"); a failed send now carries the real reason (ChatMessage::
fail_reason, set from onChatBroadcastResult via friendlyRpcError, in-memory
only) in the "not sent" hover tooltip.
- Explorer: a block height beyond the tip says so ("Block N is beyond the chain
tip (M)") instead of silently snapping to the top; the empty block list shows
Not connected / Syncing (N behind) / Loading instead of an endless "Loading".
- Settings: the misleading "Reset to Defaults" button (which only reloads the
last-saved values) is relabeled "Discard unsaved changes" with accurate copy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2927,8 +2927,10 @@ void RenderSettingsPage(App* app) {
|
||||
Notifications::instance().success(TR("settings_saved"));
|
||||
}
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_settings"));
|
||||
af.next(material::ActionButtonWidth(TR("reset_to_defaults"), ICON_MD_RESTORE));
|
||||
if (material::ActionButton("##aboutreset", TR("reset_to_defaults"), ICON_MD_RESTORE, AT::Tertiary)) {
|
||||
// This only reloads the last-saved values from disk (it doesn't apply factory defaults), so
|
||||
// label it honestly as "Discard unsaved changes" rather than the misleading "Reset to Defaults".
|
||||
af.next(material::ActionButtonWidth(TR("discard_changes"), ICON_MD_UNDO));
|
||||
if (material::ActionButton("##aboutreset", TR("discard_changes"), ICON_MD_UNDO, AT::Tertiary)) {
|
||||
if (app->settings()) { loadSettingsPageState(app); Notifications::instance().info(TR("settings_reloaded")); }
|
||||
}
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reset_settings"));
|
||||
|
||||
@@ -1679,8 +1679,15 @@ void RenderChatTab(App* app)
|
||||
const char* glyph = sending ? ICON_MD_SCHEDULE : ICON_MD_DONE;
|
||||
const float gs = icfSz * 0.82f;
|
||||
const ImVec2 gsz = icf->CalcTextSizeA(gs, FLT_MAX, 0.0f, glyph);
|
||||
dl->AddText(icf, gs, ImVec2(bmax.x - gsz.x, bmax.y + 1.0f * dp),
|
||||
material::WithAlpha(material::OnSurface(), sending ? 95 : 135), glyph);
|
||||
const ImVec2 gpos(bmax.x - gsz.x, bmax.y + 1.0f * dp);
|
||||
dl->AddText(icf, gs, gpos,
|
||||
material::WithAlpha(material::OnSurface(), sending ? 170 : 200), glyph);
|
||||
// Explain the glyph on hover — a check means "broadcast to the network", not "delivered".
|
||||
const float hpad = 4.0f * dp;
|
||||
if (ImGui::IsWindowHovered() &&
|
||||
ImGui::IsMouseHoveringRect(ImVec2(gpos.x - hpad, gpos.y - hpad),
|
||||
ImVec2(gpos.x + gsz.x + hpad, gpos.y + gsz.y + hpad)))
|
||||
material::Tooltip("%s", TR(sending ? "chat_status_sending" : "chat_status_sent"));
|
||||
ImGui::Dummy(ImVec2(0.0f, gs + 2.0f * dp)); // reserve the status glyph (no ItemSpacing now)
|
||||
}
|
||||
// Failed send → right-aligned "not sent" + Retry (kept — it's actionable).
|
||||
@@ -1692,6 +1699,12 @@ void RenderChatTab(App* app)
|
||||
ImGui::PushFont(metaFont); ImGui::PushStyleColor(ImGuiCol_Text, material::Error());
|
||||
ImGui::TextUnformatted(TR("chat_send_failed"));
|
||||
ImGui::PopStyleColor(); ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
std::string tip = m.fail_reason.empty()
|
||||
? std::string(TR("chat_failed_generic"))
|
||||
: std::string(TR("chat_failed_generic")) + "\n" + m.fail_reason;
|
||||
material::Tooltip("%s", tip.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TR("chat_retry"))) {
|
||||
// A failed contact request must be re-sent as a request (it has no peer key
|
||||
|
||||
@@ -424,7 +424,15 @@ static void performSearch(App* app, const std::string& query) {
|
||||
try { h = std::stol(query); } catch (...) { return; }
|
||||
int tip = app->getWalletState().sync.blocks;
|
||||
if (h < 1) h = 1;
|
||||
if (tip > 0 && h > tip) h = tip;
|
||||
if (tip > 0 && h > tip) {
|
||||
// Say the height is beyond the tip instead of silently snapping to the top of the list.
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), TR("explorer_beyond_tip"),
|
||||
(int)std::min<long>(h, 2147483647L), tip);
|
||||
s_search_error = msg;
|
||||
s_search_target_height = -1; // don't re-anchor; leave the list where it is
|
||||
return;
|
||||
}
|
||||
s_search_target_height = static_cast<int>(h);
|
||||
s_recent_page = 0;
|
||||
s_search_error.clear();
|
||||
@@ -939,7 +947,30 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
|
||||
if (blocks.empty()) {
|
||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
||||
ImGui::TextDisabled("%s", fuzzyMode ? TR("explorer_no_results") : TR("loading"));
|
||||
// Say WHY the list is empty — offline, syncing, or a genuine search miss — instead of an
|
||||
// endless "Loading" that also shows when the node is disconnected or still catching up.
|
||||
const auto& st = app->getWalletState();
|
||||
auto* erpc = app->rpc();
|
||||
char emptyBuf[128];
|
||||
const char* msg = TR("loading");
|
||||
ImU32 col = OnSurfaceMedium();
|
||||
if (fuzzyMode) {
|
||||
msg = TR("explorer_no_results");
|
||||
} else if (!st.connected || !erpc || !erpc->isConnected()) {
|
||||
msg = TR("explorer_empty_not_connected"); col = Warning();
|
||||
} else if (st.sync.syncing) {
|
||||
const int behind = st.sync.headers - st.sync.blocks;
|
||||
if (st.sync.tip_known && behind > 0) {
|
||||
snprintf(emptyBuf, sizeof(emptyBuf), TR("explorer_empty_syncing"), behind);
|
||||
msg = emptyBuf;
|
||||
} else {
|
||||
msg = TR("explorer_empty_syncing_unknown");
|
||||
}
|
||||
col = Warning();
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, col);
|
||||
ImGui::TextUnformatted(msg);
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
for (size_t i = 0; i < blocks.size(); i++) {
|
||||
const auto& row = blocks[i];
|
||||
|
||||
Reference in New Issue
Block a user