feat(ui): friendly RPC errors + pause-on-hover/ellipsis toasts
Two of the systemic error-surfacing foundations from the UX audit: - util::friendlyRpcError() maps the common raw daemon/JSON-RPC errors (warming up / -28, locked / -13, method-not-found / -32601, insufficient funds, unreachable node) to short translated guidance with a next step, falling back to the trimmed raw text. Wired into the send (money) path: the toast shows the friendly cause, the overlay keeps the raw text below it for diagnostics. The helper is reusable for the remaining raw-error sites. - Toasts now pause their auto-dismiss while hovered and ellipsize over-long text with the full message in a hover tooltip, instead of hard-clipping it mid-word (durations were already lengthened earlier). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,11 @@ namespace ui {
|
||||
|
||||
void Notifications::render()
|
||||
{
|
||||
// Pause-on-hover: while the pointer rests on the toast, keep it alive so a long error message can
|
||||
// actually be read (errors/warnings already last longer, but hovering holds any of them open).
|
||||
if (toast_hovered_ && !notifications_.empty())
|
||||
notifications_.back().created_at = std::chrono::steady_clock::now();
|
||||
|
||||
// Remove every expired notification; the newest remaining one is then both the last and a live one.
|
||||
pruneExpired(notifications_, std::chrono::steady_clock::now());
|
||||
|
||||
@@ -146,13 +151,35 @@ void Notifications::render()
|
||||
cursorX += iconW + iconGap;
|
||||
}
|
||||
|
||||
// Message text (clipped to pill bounds)
|
||||
// Message text — ellipsized to fit the (viewport-clamped) pill rather than hard-clipped mid-word,
|
||||
// with the full text shown in a tooltip on hover.
|
||||
bool clipped = false;
|
||||
if (textFont) {
|
||||
ImU32 textCol = ImGui::ColorConvertFloat4ToU32(text_color);
|
||||
const float availTextW = (pMax.x - pillPadX) - cursorX;
|
||||
std::string shown = notif.message;
|
||||
if (availTextW > 0.0f &&
|
||||
textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, shown.c_str()).x > availTextW) {
|
||||
clipped = true;
|
||||
const std::string ell = "\xE2\x80\xA6"; // "…"
|
||||
while (!shown.empty() &&
|
||||
textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f,
|
||||
(shown + ell).c_str()).x > availTextW) {
|
||||
shown.pop_back();
|
||||
while (!shown.empty() && (static_cast<unsigned char>(shown.back()) & 0xC0) == 0x80)
|
||||
shown.pop_back(); // drop any trailing UTF-8 continuation bytes
|
||||
}
|
||||
shown += ell;
|
||||
}
|
||||
dl->PushClipRect(ImVec2(cursorX, pillY), ImVec2(pMax.x - pillPadX, pMax.y), true);
|
||||
dl->AddText(textFont, textFont->LegacySize, ImVec2(cursorX, contentY), textCol, notif.message.c_str());
|
||||
dl->AddText(textFont, textFont->LegacySize, ImVec2(cursorX, contentY), textCol, shown.c_str());
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
// Hover: pause the auto-dismiss (handled next frame) and reveal the full message if it was clipped.
|
||||
toast_hovered_ = ImGui::IsMouseHoveringRect(pMin, pMax);
|
||||
if (toast_hovered_ && clipped)
|
||||
ImGui::SetTooltip("%s", notif.message.c_str());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -192,6 +192,7 @@ private:
|
||||
|
||||
std::deque<Notification> notifications_;
|
||||
size_t max_notifications_ = 5;
|
||||
bool toast_hovered_ = false; // pointer was over the toast last frame → pause its auto-dismiss
|
||||
std::chrono::steady_clock::time_point last_drawn_{};
|
||||
std::function<void(const std::string&, bool)> console_callback_;
|
||||
|
||||
|
||||
@@ -927,10 +927,14 @@ void RenderSendConfirmPopup(App* app) {
|
||||
s_memo[0] = '\0';
|
||||
s_send_max = false;
|
||||
} else {
|
||||
s_tx_status = std::string(TR("send_error_prefix")) + result;
|
||||
// Show a friendly cause in the toast; keep the raw backend text below it in
|
||||
// the overlay (copyable) so the detail isn't lost for diagnostics.
|
||||
const std::string friendly = util::friendlyRpcError(result);
|
||||
s_tx_status = std::string(TR("send_error_prefix")) + friendly +
|
||||
(friendly != result ? "\n\n" + result : std::string());
|
||||
s_result_txid.clear();
|
||||
s_status_success = false;
|
||||
Notifications::instance().error(std::string(TR("send_tx_failed")) + result);
|
||||
Notifications::instance().error(std::string(TR("send_tx_failed")) + friendly);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user