v1.2.0: UX audit — security fixes, accessibility, and polish
Security (P0): - Fix sidebar remaining interactive behind lock screen - Extend auto-lock idle detection to include active widget interactions - Distinguish missing PIN vault from wrong PIN; auto-switch to passphrase Blocking UX (P1): - Add 15s timeout for encryption state check to prevent indefinite loading - Show restart reason in loading overlay after wallet encryption - Add Force Quit button on shutdown screen after 10s - Warn user if embedded daemon fails to start during wizard completion Polish (P2): - Use configured explorer URL in Receive tab instead of hardcoded URL - Increase request memo buffer from 256 to 512 bytes to match Send tab - Extend notification duration to 5s for critical operations (tx sent, wallet encrypted, key import, backup, export) - Add Reduce Motion accessibility setting (disables page fade + balance lerp) - Show estimated remaining time during mining thread benchmark - Add staleness indicator to market price data (warning after 5 min) New i18n keys: incorrect_pin, incorrect_passphrase, pin_not_set, restarting_after_encryption, force_quit, reduce_motion, tt_reduce_motion, ago, wizard_daemon_start_failed
This commit is contained in:
48
src/app.cpp
48
src/app.cpp
@@ -316,7 +316,8 @@ void App::update()
|
||||
// Track user interaction for auto-lock
|
||||
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
|
||||
io.MouseClicked[0] || io.MouseClicked[1] ||
|
||||
io.InputQueueCharacters.Size > 0) {
|
||||
io.InputQueueCharacters.Size > 0 ||
|
||||
ImGui::IsAnyItemActive()) {
|
||||
last_interaction_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
@@ -1024,7 +1025,8 @@ void App::render()
|
||||
bool prevCollapsed = sidebar_collapsed_;
|
||||
{
|
||||
PERF_SCOPE("Render.Sidebar");
|
||||
ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_);
|
||||
ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_,
|
||||
state_.isLocked());
|
||||
}
|
||||
if (sbStatus.exitClicked) {
|
||||
requestQuit();
|
||||
@@ -1039,7 +1041,7 @@ void App::render()
|
||||
|
||||
// Page transition: detect change, ramp alpha
|
||||
if (current_page_ != prev_page_) {
|
||||
page_alpha_ = ui::effects::isLowSpecMode() ? 1.0f : 0.0f;
|
||||
page_alpha_ = (ui::effects::isLowSpecMode() || (settings_ && settings_->getReduceMotion())) ? 1.0f : 0.0f;
|
||||
prev_page_ = current_page_;
|
||||
}
|
||||
if (page_alpha_ < 1.0f) {
|
||||
@@ -1094,6 +1096,18 @@ void App::render()
|
||||
// Lock screen — covers tab content just like the loading overlay
|
||||
renderLockScreen();
|
||||
} else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
|
||||
// Track how long we've been waiting for encryption state
|
||||
if (state_.connected && !state_.encryption_state_known) {
|
||||
encryption_check_timer_ += ImGui::GetIO().DeltaTime;
|
||||
if (encryption_check_timer_ >= 15.0f) {
|
||||
DEBUG_LOGF("[App] Encryption state check timed out after 15s — assuming unencrypted\n");
|
||||
state_.encryption_state_known = true;
|
||||
state_.encrypted = false;
|
||||
encryption_check_timer_ = 0.0f;
|
||||
}
|
||||
} else {
|
||||
encryption_check_timer_ = 0.0f;
|
||||
}
|
||||
// Reset lock screen focus flag so it auto-focuses next time
|
||||
lock_screen_was_visible_ = false;
|
||||
// Show loading overlay instead of tab content
|
||||
@@ -2403,11 +2417,14 @@ void App::renderShutdownScreen()
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.06f, 0.06f, 0.08f, 0.92f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::Begin("##ShutdownOverlay", nullptr,
|
||||
ImGuiWindowFlags shutdownFlags =
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGuiWindowFlags_NoSavedSettings;
|
||||
// Allow input after 10s so Force Quit button is clickable
|
||||
if (shutdown_timer_ < 10.0f)
|
||||
shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
||||
ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags);
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
// Convert local centre to screen coords for draw-list primitives
|
||||
@@ -2505,6 +2522,25 @@ void App::renderShutdownScreen()
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// 4b. Force Quit button — appears after 10 seconds
|
||||
// -------------------------------------------------------------------
|
||||
if (shutdown_timer_ >= 10.0f) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
const char* forceLabel = TR("force_quit");
|
||||
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
|
||||
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
||||
if (ImGui::Button(forceLabel, btnSize)) {
|
||||
DEBUG_LOGF("Force quit requested by user after %.0fs\n", shutdown_timer_);
|
||||
shutdown_complete_ = true;
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user