UI/UX Part 24 — Error, Loading, Empty, Offline & Degraded-State UX Architecture
Reusable P2P Communication Platform
Status: UI/UX architecture specification
UI Series: Part 24
Desktop UI: Dioxus
Android UI: Kotlin + Jetpack Compose
Core runtime: Rust
Primary purpose: define the complete error/loading/empty/offline/degraded-state UX architecture across messaging, calls, files, search, contacts, groups, pairing, notifications, security, backup, emergency, settings, plugins, diagnostics, and all future product surfaces.
1. Purpose
Failure states are part of the normal product, especially in a local-first P2P system.
Users will regularly encounter:
no Internet
peer unavailable
relay unavailable
local-only connectivity
background delivery delayed
storage pressure
permission denied
partial sync
stale data
retrying transfer
search index rebuilding
plugin crashed
backup destination missing
camera unavailable
message queued
call reconnecting
These should not all become:
Something went wrong
The governing principle is:
The UX must explain what is known, what still works, what is waiting, and what the user can do next.
2. Architectural Position
Rust Domain / Runtime State
│
├── loading phase
├── data readiness
├── connectivity
├── degradation
├── failure class
├── retryability
├── recovery action
└── data-safety state
│
▼
Failure-State Presentation Service
│
┌────┴─────┐
│ │
Dioxus Compose
Desktop Android
3. Core State Families
Recommended:
#![allow(unused)] fn main() { pub enum UiDataState<T> { Loading(LoadingStateView), Ready(T), Empty(EmptyStateView), Degraded { data: T, degradation: DegradedStateView, }, Failed(ErrorStateView), } }
4. Do Not Treat Degraded as Failed
Example:
No Internet
LAN messaging still works
is:
Degraded
not:
Failed
5. Loading Taxonomy
Loading should describe phase when useful.
#![allow(unused)] fn main() { pub enum LoadingPhase { Starting, ReadingLocalData, Connecting, Syncing, Verifying, Preparing, Rebuilding, WaitingForPermission, } }
6. Initial Loading
Use only when no usable data exists yet.
7. Local-First Loading
Preferred startup:
show local snapshot immediately
then sync in background
instead of blocking on network.
8. Skeleton Loading
Use only when structure is known.
Examples:
conversation list
contact list
file grid
9. Spinner Loading
Use for small indeterminate operations.
10. Progress Loading
Use when meaningful total exists.
Examples:
backup
restore
transfer
index rebuild
11. Loading Copy
Prefer:
Loading messages…
Verifying backup…
Rebuilding search…
over generic:
Loading…
12. Avoid Fake Progress
If total unknown:
indeterminate
13. Loading Timeout
Long loading should transition to explanatory state.
Example:
Still trying to connect…
with options.
14. Background Loading
Do not block navigation for background:
sync
index
thumbnail generation
unless required.
15. Refresh State
When existing data visible:
keep content
show small refresh indicator
16. Empty State Taxonomy
Empty is not failure.
Examples:
No conversations yet
No contacts
No files
No search results
No blocked users
No security events
No plugins installed
17. Empty State Structure
Recommended:
Title
Explanation
Primary action if useful
Secondary guidance if useful
18. Good Empty State
No contacts yet
Add someone by QR, nearby, or invite code.
[Add Contact]
19. Bad Empty State
Nothing here
20. Search Empty State
Differentiate:
No results
from:
Search index still building
21. Inbox Empty State
Could be:
No conversations yet
not:
No messages found
22. Filtered Empty State
Example:
No unread conversations
with:
Clear filter
23. Offline State
Offline is a connectivity condition, not global product failure.
24. Offline Modes
Potential:
#![allow(unused)] fn main() { pub enum OfflineModeView { InternetUnavailable, LocalNetworkOnly, NearbyOnly, DtnOnly, FullyDisconnected, } }
25. Internet Unavailable
If relay/LAN/nearby remains:
No Internet
Local communication may still work
26. Local Network Only
Show:
Connected locally
Internet services unavailable
27. Nearby Only
Show:
Nearby messaging available
28. DTN Only
Show:
Messages will be stored and forwarded when a route appears
only if true.
29. Fully Disconnected
Show:
No connection right now
Messages will remain queued
30. Offline Banner
Should be compact and non-blocking.
31. Offline Banner Content
Example:
No Internet — local messaging is still available
32. Do Not Spam Offline Errors
One persistent state is better than per-action error popups.
33. Queued Message UX
User sends offline:
message appears immediately
Queued
34. Offline Success Semantics
The action can succeed locally even if network delivery waits.
35. Degraded State
Examples:
relay only
background restricted
low storage
search stale
plugin disabled
camera unavailable
call audio-only fallback
36. Degradation Model
#![allow(unused)] fn main() { pub struct DegradedStateView { pub area: DegradedArea, pub reason: DegradedReason, pub impact: String, pub actions: Vec<RecoveryActionView>, } }
37. Degraded Area
#![allow(unused)] fn main() { pub enum DegradedArea { Network, Messaging, Calls, Files, Search, Background, Storage, Security, Plugins, Backup, } }
38. Degraded Reason
Examples:
RelayOnly
StorageLow
IndexStale
PermissionDenied
BackgroundRestricted
PluginUnavailable
39. Impact Must Be Clear
Example:
Messages still work, but file transfers may be slower.
40. Error Taxonomy
Errors should be typed.
#![allow(unused)] fn main() { pub enum UiErrorKind { Validation, Permission, Connectivity, Timeout, NotFound, Conflict, Storage, Security, Compatibility, RateLimited, Cancelled, Internal, } }
41. Recoverability
#![allow(unused)] fn main() { pub enum ErrorRecoverability { Retryable, ActionRequired, Terminal, } }
42. Data Safety
Critical dimension.
#![allow(unused)] fn main() { pub enum DataSafetyState { Safe, Pending, AtRisk, Unknown, } }
43. Error View
#![allow(unused)] fn main() { pub struct ErrorStateView { pub kind: UiErrorKind, pub title: String, pub explanation: String, pub recoverability: ErrorRecoverability, pub data_safety: DataSafetyState, pub actions: Vec<RecoveryActionView>, } }
44. Error Copy Should Answer
What failed?
Is my data safe?
What can I do?
45. Example — Message Send Failed
Message couldn't be sent
Your draft is safe.
[Retry]
46. Example — Backup Failed
Backup failed
Your previous verified backup is unchanged.
[Retry]
47. Example — Restore Failed
Restore could not be completed
Your current data was not changed.
if transaction rollback succeeded.
48. Internal Error
Normal users do not need stack traces.
Show:
Something failed unexpectedly
Your data is safe
Retry
Export Diagnostics
where appropriate.
49. Inline vs Banner vs Blocking
Use different surfaces.
50. Inline Error
Use for local component/input issue.
Examples:
invalid invite code
attachment too large
wrong recovery key
51. Banner
Use for screen-level persistent degradation.
Examples:
offline
background restricted
search index stale
52. Snackbar/Toast
Use for short transient feedback.
Examples:
Copied
Archived
Retry started
53. Dialog
Use when user decision required.
Examples:
storage full during restore
device revoke confirmation
54. Full-Screen Blocking Error
Reserved for:
account revoked
critical storage corruption
unsupported app state
security lockout
55. Error Surface Escalation
Start with least disruptive surface compatible with severity.
56. Retry Architecture
Retry must be idempotent where possible.
57. Retry Action
#![allow(unused)] fn main() { pub enum RecoveryActionView { Retry, Refresh, Reconnect, OpenSettings, FreeStorage, RebuildSearch, ChooseAnotherDestination, Reauthenticate, UpdateApp, ExportDiagnostics, ContactSupport, } }
58. Retry Button
Do not create duplicate messages/transfers/jobs.
59. Automatic Retry
Background retry can continue silently for:
message delivery
sync
relay connection
transfer resume
60. Manual Retry
Expose when user action adds value.
61. Backoff
Do not show technical backoff values normally.
62. Retry Countdown
If rate-limited:
Try again in 30 seconds
if reliable.
63. Rate Limit
Do not repeatedly enable a button known to fail.
64. Cancelled Is Not Error
User cancellation:
Cancelled
should not show failure styling unless partial risk exists.
65. Timeout
Explain what timed out.
Example:
Could not reach this device
Try again
66. Network Timeout With Fallback
If relay fallback exists:
Direct connection failed
Using relay instead
degraded, not failed.
67. Partial Data
Local-first systems may have incomplete history.
68. Partial Data State
Example:
Recent messages available
Older history is still syncing
69. Partial Search
Example:
Search results may be incomplete while history syncs
70. Partial File Availability
Example:
File metadata available
Content not downloaded
71. Stale Data
Some state can be usable but old.
72. Stale Presence
Use:
Last seen recently
not falsely current.
73. Stale Search Index
Show:
Some recent content may be missing from search
74. Stale Device List
High-risk security state should refresh before destructive action.
75. Stale Settings
Rust revalidates before commit.
76. Stale Restore Plan
Show:
This restore plan is no longer current
Review again
77. Permission Failure
Must explain:
what permission enables
what still works
78. Camera Denied
Camera access is off
You can enter the code manually.
79. Microphone Denied
Microphone access is required for voice notes
Text messaging still works.
80. Notification Denied
You may miss message and call alerts in the background.
81. Nearby Denied
Nearby discovery is unavailable
QR/manual code still work.
82. Location Denied in SOS
SOS will be sent without location.
83. Permission Recovery
Offer:
Allow
Open Settings
Use Alternative
84. Storage Pressure
Important degraded state.
85. Storage Levels
#![allow(unused)] fn main() { pub enum StoragePressureState { Normal, Low, Critical, Full, } }
86. Low Storage
Storage is running low
Large downloads may be paused.
87. Critical Storage
Storage is critically low
Backups/transfers may fail.
88. Storage Full
Block operations that cannot safely continue.
89. Storage Recovery
Offer:
Manage Storage
Clear Cache
Clear Partial Transfers
Choose Another Destination
90. Do Not Auto-Delete Authoritative Data
Hard rule.
91. Search Rebuild State
Search is rebuilding
Messaging remains available.
92. Search Failure
If index corrupt:
Search unavailable
Rebuild Index
93. Plugin Failure
Plugin-specific.
94. Plugin Crashed
Translator Plugin stopped unexpectedly
Core messaging is unaffected.
95. Plugin Quarantined
Plugin disabled for security reasons
96. Plugin Extension Missing
Core UI remains usable.
97. Background Delivery Degraded
Android example:
Background delivery may be delayed
Battery restrictions are enabled.
98. Foreground Still Works
Explain:
Messages will sync when the app is open.
if true.
99. Daemon Stopped Desktop
Background service is stopped
Notifications and background delivery are unavailable.
100. Restart Daemon
Safe recovery action.
101. Call Degradation
Calls often degrade gradually.
102. Call States
Examples:
Video quality reduced
Audio only
Reconnecting
Relay connection
Camera unavailable
103. Call Reconnecting
Show:
Reconnecting…
while preserving same CallId.
104. Call Failure
After retry window:
Call ended because connection was lost
105. Camera Failure During Call
Camera unavailable
Audio call continues.
106. Microphone Failure
Critical for call.
Show actionable:
Microphone unavailable
Check permission/device
107. File Transfer Degradation
Examples:
Waiting for sender
Paused on mobile data
Waiting for Wi-Fi
Storage full
Verification failed
108. Waiting Is Not Failure
Hard rule.
109. Transfer Verification Failure
Strong:
File could not be verified
It was not opened or marked complete.
110. Backup Degradation
Examples:
destination unavailable
previous backup still safe
backup stale
verification pending
111. Restore Blocking Failure
If compatibility/security fails:
restore blocked
not degraded.
112. Emergency Degraded State
Examples:
No Internet — nearby relay available
No route right now — SOS stored
Location unavailable
113. Emergency Error Honesty
Never show:
SOS failed
if it is safely stored and still eligible for relay.
114. Security Failure
Security errors should fail closed.
115. Verification Mismatch
Codes do not match
Do not continue.
116. Identity Changed
Persistent warning until reviewed.
117. Device Revoked
Blocking state:
This device no longer has access.
118. Recovery Invalid
Critical.
119. Error Severity
#![allow(unused)] fn main() { pub enum UiErrorSeverity { Info, Warning, Error, Critical, } }
120. Severity Does Not Determine Surface Alone
A critical background security issue may need banner/notification/full screen depending context.
121. State Persistence
Errors must survive restart if underlying issue persists.
122. Transient Errors
Do not persist forever.
Example:
temporary relay timeout
123. Durable Errors
Examples:
security warning
backup failed
device revoked
storage corruption
may persist.
124. Error Identity
#![allow(unused)] fn main() { pub struct UiIssueId(pub Uuid); }
Useful for deduplication.
125. Duplicate Errors
Do not show repeated identical banners/snackbars.
126. Error Coalescing
Example:
10 failed message retries
can become:
3 messages need attention
127. Burst Failures
Plugin/network storms should not create notification storm.
128. Recovery Progress
After user acts:
Reconnecting…
Rebuilding…
Retrying…
129. Recovery Success
Use concise confirmation:
Connected
Search rebuilt
Plugin restarted
130. Recovery Failure
Keep prior issue visible with updated reason.
131. Loading + Error Transition
A component should not oscillate:
loading → error → loading → error
every retry tick.
Use stable retry state.
132. Error Hysteresis
For flaky connectivity, avoid banner flicker.
133. Offline Hysteresis
Short network blip may not immediately show full offline banner.
134. Foreground vs Background
Foreground can show more detail.
Background may rely on Part 13 notifications.
135. Notification Failure
If notification permission unavailable:
in-app warning
not repeated system popups.
136. Startup Error Architecture
Startup phases:
Loading local state
Starting runtime
Opening database
Restoring session
137. Startup Local Data Failure
If DB temporarily locked:
Retrying local data…
138. Startup Corruption
Blocking screen with safe options:
Run Repair
Restore Backup
Export Diagnostics
if supported.
139. Startup Network Failure
Should not block local-first app startup.
140. Startup Plugin Failure
Start without plugin if possible.
141. Safe Mode
From Part 19.
142. Crash Recovery
After crash:
restore durable state
preserve drafts
resume jobs where safe
143. Crash Message
If needed:
The app restarted unexpectedly
Your messages are safe.
only if known.
144. Unknown Data Safety
If uncertain:
We could not confirm whether the last operation completed.
Review status before retrying.
145. Optimistic UI Failure
Example message send:
pending row remains
failed state
retry
146. Rollback
For reversible settings/action failure:
restore previous state
147. High-Risk Action Failure
Do not optimistically commit.
148. Error Presentation API
#![allow(unused)] fn main() { pub trait FailureStatePresentation { async fn issue( &self, id: UiIssueId, ) -> Result<ErrorStateView, UiError>; async fn recover( &self, id: UiIssueId, action: RecoveryActionView, ) -> Result<RecoveryResultView, UiError>; } }
149. Global Degradation API
#![allow(unused)] fn main() { pub trait DegradationPresentation { async fn current( &self, ) -> Result<Vec<DegradedStateView>, UiError>; } }
150. Loading View
#![allow(unused)] fn main() { pub struct LoadingStateView { pub phase: LoadingPhase, pub progress: Option<f32>, pub cancellable: bool, } }
151. Empty View
#![allow(unused)] fn main() { pub struct EmptyStateView { pub kind: EmptyStateKind, pub primary_action: Option<EmptyStateAction>, } }
152. Empty State Kind
#![allow(unused)] fn main() { pub enum EmptyStateKind { NoConversations, NoContacts, NoFiles, NoSearchResults, NoSecurityEvents, NoPlugins, NoBackups, NoBlockedContacts, } }
153. Offline Snapshot
#![allow(unused)] fn main() { pub struct OfflineStateView { pub mode: OfflineModeView, pub queued_messages: u32, pub local_features_available: Vec<OfflineCapability>, } }
154. Offline Capability
Examples:
LANMessaging
NearbyMessaging
ReadLocalHistory
CreateDrafts
SearchLocal
ViewFiles
155. UI Event Model
#![allow(unused)] fn main() { pub enum FailureUiEvent { IssueAdded(ErrorStateView), IssueUpdated(ErrorStateView), IssueResolved(UiIssueId), DegradationChanged(Vec<DegradedStateView>), OfflineChanged(OfflineStateView), } }
156. Android ViewModel
Owns:
banner visibility
snackbar queue
permission-navigation effects
retry button presentation
Rust owns:
failure semantics
retryability
data safety
recovery action validity
157. Dioxus Presenter
Owns:
inline errors
desktop banners
dialogs
focus after recovery
158. Do Not Infer Retryability in UI
Hard rule.
159. Do Not Infer Data Safety in UI
Hard rule.
160. Error Localization
Rust can expose typed reason.
UI localizes natural explanation.
161. Error Codes
Developer diagnostics may expose stable:
E_NET_RELAY_UNAVAILABLE
Normal UI does not.
162. Error Correlation ID
Support may use:
Issue ID
without private content.
163. Accessibility — Loading
Screen reader announces:
Loading conversations
once.
164. Loading Progress Accessibility
Do not announce every percent.
165. Accessibility — Empty
Example:
No contacts yet. Add a contact.
166. Accessibility — Error
Example:
Backup failed. Previous backup is safe. Retry button.
167. Accessibility — Offline
No Internet. Nearby messaging is available.
168. Focus on Error
Do not steal focus for ordinary inline/banner errors.
169. Blocking Error
Focus moves to heading/actions.
170. Snackbar Accessibility
Must remain long enough and expose action.
171. Color Independence
All failure states use:
text
icon
semantic label
not red/yellow alone.
172. Large Text
Error explanations wrap.
173. RTL
Localized error text mirrors.
Technical codes preserve direction.
174. Reduced Motion
Loading/retry animations can become static.
175. Cognitive Load
Show one primary recovery action.
Secondary actions beneath.
176. Error Message Style
Avoid:
Unknown error
Invalid state
Operation failed
when more useful semantic reason exists.
177. Avoid Blame
Do not say:
You failed to…
Prefer:
Camera access is off
178. Offline Wording
Avoid:
You're offline
if local/mesh routes exist.
179. Partial Success Wording
Example:
Message saved locally and will send when a route is available.
180. Safety Wording
Example:
Your draft is safe.
only when Rust confirms.
181. Diagnostics Link
Persistent/repeated failures may offer:
View Diagnostics
182. Support Bundle Link
For unexplained failures:
Export Diagnostics
183. No Automatic Error Upload
Hard rule.
184. Error Telemetry
Do not log private payloads.
185. Safe Metrics
Possible:
error class
retry count
recovery success
loading latency
without content/identity.
186. Crash Reports
Redacted.
187. Testing Matrix
Required:
initial loading
background refresh
empty
filtered empty
offline Internet-only
LAN-only
nearby-only
fully disconnected
degraded relay-only
permission denied
storage low/full
search stale/corrupt
plugin crash
background restricted
188. Messaging Tests
queued
sending
failed
retry
duplicate retry
offline send
189. Call Tests
reconnecting
relay-only
camera failure
microphone failure
network loss
190. File Tests
waiting for sender
Wi-Fi-only pause
storage full
verification failure
191. Backup Tests
destination unavailable
storage full
verify fail
retry
previous backup safe
192. Security Tests
identity mismatch
device revoked
recovery invalid
193. Android Tests
permission denial
battery restriction
process death
background delivery degraded
TalkBack
large text
194. Desktop Tests
daemon stopped
network loss
window restore
keyboard
safe mode
195. Accessibility Tests
All recovery actions usable via:
TalkBack
keyboard
large text
RTL
196. Error Dedup Tests
Repeated same issue shows one persistent state.
197. Hysteresis Tests
Short network flaps do not create banner flicker.
198. Data Safety Tests
Every destructive/failed operation reports correct:
Safe
Pending
AtRisk
Unknown
199. Loading Performance
Local snapshot should render quickly.
200. Error Performance
Failure rendering must not block core recovery work.
201. Event Flood
Hundreds of retry updates should be coalesced.
202. Initial Production Scope
Ship:
typed loading states
meaningful empty states
offline/local-only banner
degraded-state model
typed error classes
retryability
data-safety semantics
permission failures
storage pressure
sync/queue states
call degradation
transfer waiting/failure
backup/search/plugin degradation
startup/crash recovery states
accessibility
Defer:
complex predictive remediation
AI-generated error explanations
automatic destructive repair
203. Definition of Done
UI/UX Part 24 is complete when:
- loading, empty, offline, degraded, failed, and cancelled states are distinct
- local-first startup renders usable local data before network sync where possible
- empty states explain what is empty and provide appropriate next action
- no-Internet state does not imply total failure when LAN/nearby/DTN still works
- degraded states clearly explain impact and what continues to work
- errors are typed by category, recoverability, severity, and data-safety state
- retry actions are Rust-authoritative and idempotent
- user cancellation is not styled as failure
- permission failures always provide alternatives where available
- storage pressure never causes silent deletion of authoritative data
- queued/waiting states are not mislabeled as failure
- security failures fail closed
- startup network failure does not block local-first operation
- plugin failure cannot take down core UX
- duplicate errors and network flaps are coalesced/hysteresis-controlled
- accessibility, large text, RTL, reduced motion, and screen-reader semantics are defined
- Rust failure/degradation/offline presentation contracts are specified
- data-safety wording is shown only when confirmed by Rust
- offline/degraded/permission/storage/crash/retry/deduplication tests are included
204. Final Architecture
RUST PRODUCT STATE
│
┌───────────────────────┼───────────────────────┐
│ │ │
Loading Degraded Failed
│ │ │
Phase/Progress Impact/What Works Cause/Recovery
│ │ │
└───────────────────────┼───────────────────────┘
│
Failure-State Presentation
┌─────────┴─────────┐
│ │
Dioxus Compose
Desktop Android
The key UX sequence is:
What happened?
→ What still works?
→ Is my data safe?
→ What should I do next?
205. Final Principle
Failure states should reduce uncertainty rather than add to it.
The correct model is:
typed state
+
clear impact
+
honest data-safety status
+
one useful recovery path
+
local-first continuity
not:
spinner
→ generic error
→ retry
→ generic error
This gives Dioxus desktop and Android Compose resilient behavior that remains understandable even when connectivity, permissions, storage, background execution, plugins, or other subsystems are partially unavailable.