UI/UX Part 14 — Presence, Typing, Receipts & Status UX Architecture
Reusable P2P Communication Platform
Status: UI/UX architecture specification
UI Series: Part 14
Desktop UI: Dioxus
Android UI: Kotlin + Jetpack Compose
Core runtime: Rust
Primary purpose: define the complete user-facing presence, typing, recording, delivery/read receipts, availability/status, last-seen, privacy, TTL/staleness, group aggregation, multi-device semantics, offline ambiguity, accessibility, and Rust presentation contracts across desktop and Android.
1. Purpose
Presence and receipts look simple in the UI, but they are among the easiest features to make misleading.
A user should not infer certainty where the distributed system only has partial information.
Examples:
"offline" may actually mean unknown
"last seen 2 minutes ago" may be stale
"typing" must expire
"read" is usually sequence-based, not per-message polling
one user may have several devices
a muted device may still be online
a group may have too many participants for per-user live state
The governing principle is:
Ephemeral social state should communicate useful hints without pretending to be stronger evidence than the protocol actually provides.
2. Architectural Position
Rust Presence / Ephemeral State
│
├── account presence
├── device observations
├── availability
├── typing
├── recording
├── read cursors
└── delivery state
│
▼
Presence Presentation Service
│
┌────┴─────┐
│ │
Dioxus Compose
Desktop Android
3. Presence vs Availability
These are related but distinct.
Presence:
Online
Away
Unknown
Availability:
Available
Busy
Do Not Disturb
Invisible
Do not collapse both into one dot.
4. Presence State
Recommended:
#![allow(unused)] fn main() { pub enum PresenceState { Online, Away, Unknown, } }
Avoid hard Offline unless the protocol can prove it.
5. Why Unknown Matters
A device may be:
asleep
out of network
battery constrained
behind delayed transport
not publishing presence
So absence of presence is not always proof of offline state.
6. Last Seen
Optional feature.
#![allow(unused)] fn main() { pub struct LastSeenView { pub at: Timestamp, pub confidence: PresenceConfidence, } }
7. Last Seen Privacy
Users may choose:
Everyone
Contacts
Nobody
or equivalent.
8. Last Seen Precision
Recommended user-facing formatting:
just now
5 minutes ago
today
yesterday
recently
depending privacy setting.
Avoid exact minute precision if policy intentionally coarsens.
9. Presence Confidence
#![allow(unused)] fn main() { pub enum PresenceConfidence { Live, Recent, Stale, } }
Normal UI rarely shows this directly.
It affects wording.
10. Presence TTL
Rust owns expiry.
UI does not run its own truth-deciding timer.
11. Presence Update Flow
network observation
→ Rust presence aggregator
→ TTL/hysteresis
→ PresenceSummary
→ UI
12. Multi-Device Presence
A user may have:
phone online
desktop asleep
tablet offline
UI should usually show account-level aggregate.
13. Account-Level Presence
Possible policy:
Online if any authorized device is live
Away if at least one recent device exists but none active
Unknown otherwise
Exact rule belongs in Rust.
14. Do Not Show Per-Device Presence by Default
Normal contact UI should not expose:
Alice's phone online
Alice's laptop away
unless advanced security/device detail is intended.
15. Presence Locations
Appropriate surfaces:
contact profile
conversation header
small contact row indicator
group member list for small groups
16. Presence in Inbox
Optional small indicator.
Must not reorder conversations.
17. Presence in Conversation Header
Good place for:
Online
Away
Last seen recently
18. Presence in Group
Small groups may show per-member presence.
Large groups should avoid full live-state rendering.
19. Large Group Presence
Possible summary:
12 active
or omit entirely.
20. Presence Dot Semantics
If using a dot:
dot + text/tooltip
not color-only meaning.
21. Availability State
Recommended:
#![allow(unused)] fn main() { pub enum AvailabilityState { Available, Busy, DoNotDisturb, Invisible, } }
22. Invisible
User can appear:
Unknown
to others while still receiving messages.
23. Do Not Disturb
Affects:
presence/status
notification policy
but not necessarily message delivery.
24. Busy
Informational.
May influence call UI:
Busy
but does not necessarily block calls.
25. Status Text
Optional short user-defined status:
In class
Working
Back later
26. Status Text Privacy
Treat as profile content.
Do not over-index/share beyond intended audience.
27. Status Expiry
Optional:
Clear after 1 hour
Clear tonight
Clear manually
28. Presence Settings
Recommended:
Show when online
Show last seen
Share typing indicators
Share read receipts
Show status
29. Typing Indicator
Ephemeral.
#![allow(unused)] fn main() { pub struct TypingSummary { pub participants: Vec<AccountId>, pub state: TypingState, } }
30. Typing States
#![allow(unused)] fn main() { pub enum TypingState { None, Typing, Recording, } }
For multiple users, aggregate separately if needed.
31. Typing Start
Triggered by meaningful input.
Do not start on:
opening composer
moving cursor
opening attachment picker
32. Typing Stop
On:
idle timeout
send
clear
leave conversation
focus loss
privacy disable
33. Typing TTL
Rust owns.
If stop packet is lost, TTL clears stale indicator.
34. Typing Frequency
Rate-limit updates.
Example:
start
periodic keepalive
stop
not per keystroke.
35. 1:1 Typing UX
Show:
Alice is typing…
36. Group Typing UX
Examples:
Alice is typing…
Alice and Bob are typing…
Several people are typing…
37. Typing Participant Limit
Do not list 20 names.
38. Typing Placement
Best:
near bottom of timeline / above composer
39. Typing Does Not Become Timeline Row
Hard rule.
40. Typing While User Reads History
Indicator can remain near composer.
Do not scroll timeline.
41. Recording Indicator
If peer is recording voice note:
Alice is recording audio…
42. Recording Indicator Privacy
Same privacy control as typing or separate advanced setting.
43. Attachment Preparation
Do not show:
Alice is typing…
just because peer selected a file.
44. Delivery State
Outgoing message states:
#![allow(unused)] fn main() { pub enum MessageDeliveryState { Queued, Sending, Sent, Delivered, Read, Failed, } }
45. Queued
Meaning:
durably accepted locally, waiting for route/outbox
46. Sending
Actively attempting transfer.
47. Sent
Sender-side protocol state completed.
Exact semantics documented by backend.
48. Delivered
Recipient side durably accepted according to protocol.
49. Read
Recipient read cursor advanced through message.
50. Failed
Requires attention or retry.
51. Delivery Icon UX
Possible compact visual states:
clock
single check
double check
read marker
warning
But each must have accessible text.
52. Do Not Copy Familiar Icon Semantics Blindly
If backend meaning differs, labels/details must match actual semantics.
53. Message Details
On demand show:
Queued at
Sent at
Delivered at
Read at
where available.
54. Group Delivery
Avoid per-message crowded member lists.
Use aggregate:
Delivered to 5
Read by 3
55. Group Receipt Details
Dedicated sheet/dialog can show participants for small groups.
56. Large Group Receipts
Possible policy:
aggregate only
disabled
to reduce cost/privacy.
57. Read Cursor
Recommended protocol model:
#![allow(unused)] fn main() { pub struct ReadCursor { pub conversation: ConversationId, pub through: MessageSequence, } }
58. Read Cursor Advantage
Avoids sending one receipt per message.
59. Read Receipt Semantics
If cursor is through sequence 120:
messages <= 120 are read
subject to visibility/security policy.
60. Read Detection — Desktop
Suggested criteria:
conversation active
window focused
message sufficiently visible
61. Read Detection — Android
Suggested:
Activity resumed
conversation route active
message visible
62. Opening Conversation Is Not Enough
Do not mark all unread messages read immediately on navigation.
63. Scrolling Through Unread
Advance cursor to highest actually viewed eligible sequence.
64. New Message at Bottom
If user is focused and near bottom:
may become read quickly
after visibility criteria.
65. New Message While Scrolled Up
Do not mark read.
66. Notification Is Not Read
Showing notification does not advance cursor.
67. Notification Tap
Opening conversation may advance read later based on viewport.
68. Mark Read Action
Explicitly advances cursor according to product semantics.
69. Mark Unread
As Part 04:
local reminder state
not undo of remote read receipt.
70. Read Receipt Privacy
User setting may disable sending read receipts.
71. When Read Receipts Disabled
Local unread/read still works.
Remote peer does not receive read cursor.
72. Incoming Read Receipt
If peer disabled read receipts:
Delivered
may remain highest visible state.
Do not imply not read.
73. Unknown vs Not Read
Important wording.
No receipt means:
read status unavailable
not proof of unread.
74. Group Read Privacy
May be configurable separately.
75. Read Receipt in Requests
Default:
not sent
until request accepted.
76. Typing in Requests
Default:
not sent
77. Presence in Requests
Default:
not shared
78. Blocked Contact
Presence/typing/read sharing stops according to policy.
79. Muted Conversation
Mute affects notifications.
It should not necessarily affect:
presence
typing
read receipts
unless user privacy setting says.
80. Offline State
Conversation header may show:
Offline
only if app itself has no usable path.
Peer-specific state should use Unknown if uncertain.
81. Global Connectivity vs Peer Presence
Separate.
App offline
is local connectivity.
Alice unknown
is peer presence.
82. Presence During Local LAN
Peer can be:
Online nearby
even without Internet.
Do not equate Internet with online.
83. Presence over DTN
Presence may be intentionally suppressed/stale.
UI should avoid pretending liveness.
84. Last Seen over Delayed Transport
If updates delayed, confidence may degrade to:
recently
85. Presence Hysteresis
Rust prevents flicker:
Online
Unknown
Online
Unknown
during short network changes.
86. UI Stability
Presence change should re-render only:
header
row indicator
member row
87. Typing Update Locality
Only bottom typing region / relevant row.
88. Read Receipt Update Locality
Only affected outgoing message group/details.
89. Multi-Device Read Aggregation
User's own devices may read at different positions.
Account-level read cursor can be:
max securely synchronized cursor
if backend chooses.
UI consumes result.
90. Multi-Device Typing
If same user types on multiple devices:
one account-level typing state
not duplicate:
Alice is typing twice
91. Multi-Device Presence
Same.
92. Read on Another Device
Local unread badge updates.
Do not move current scroll position.
93. Message Read on Phone, Desktop Open
Desktop can update:
read cursor
unread separator
inbox badge
without forcibly jumping.
94. Read Receipt Event
#![allow(unused)] fn main() { pub struct ReceiptUpdateView { pub conversation: ConversationId, pub participant: AccountId, pub delivered_through: Option<MessageSequence>, pub read_through: Option<MessageSequence>, } }
95. Delivery Cursor
Protocol may aggregate delivery similarly.
96. Receipt Timestamp
If exact timestamp exists, details can show.
97. Time Precision
Avoid implying exactness if backend timestamp uncertain.
98. Status/Presence in Contact List
Use subtle:
Online
Away
Only if user has enabled display.
99. Status in Conversation Header
Preferred place for richer:
Online
Busy
Last seen recently
100. Status in Group Header
Avoid individual status.
Maybe:
5 active
101. DND Status
If peer exposes DND:
Do not disturb
may inform user, but should not block send.
102. Busy Status
Likewise.
103. Invisible Status
Remote users see:
Unknown
not:
Invisible
because invisible is private state.
104. Own Presence Controls
Profile/status menu can expose:
Available
Busy
Do Not Disturb
Invisible
105. Automatic Away
Rust/platform can set away after:
desktop idle
mobile inactivity/background
according to policy.
106. Manual Status Overrides
User-set Busy/DND can override automatic presence.
107. Status Expiry
Allow:
Until changed
1 hour
Today
108. DND Integration
Application DND may also adjust:
notification behavior
but OS DND remains separate.
109. Do Not Confuse App DND with OS DND
Settings should name scope.
110. Desktop Status Menu
Can live in profile/avatar menu.
111. Android Status Menu
Profile/settings bottom sheet.
112. Custom Status
Optional v1/later.
Could include:
emoji + text
113. Custom Status Safety
Bound length.
No rich HTML.
114. Status Expiration Event
Rust clears.
UI refreshes.
115. Presence Privacy Matrix
Potential:
Online status: Contacts / Nobody
Last seen: Contacts / Nobody
Typing: On / Off
Read receipts: On / Off
116. Fine-Grained Per-Contact Overrides
Future.
Start with global settings.
117. Managed Enterprise Policy
Could enforce:
read receipts disabled
presence limited
UI displays policy-locked setting.
118. Privacy Setting Sync
Can be account-wide.
119. OS-Specific Availability
Automatic away logic may differ by platform.
Rust receives normalized lifecycle/activity hints.
120. Android Background
Do not advertise:
Online
forever merely because foreground service exists.
Presence policy decides.
121. Active Call Presence
Optional account availability:
Busy
during call.
122. Call Status Exposure
Could show:
In a call
only if user chooses.
Default may remain private.
123. Desktop Idle
Platform adapter may report:
active
idle
locked
where available.
124. Device Locked
Could transition:
Away
without revealing lock state.
125. Typing Privacy During Lock
Stop typing immediately on lock/background.
126. Recording Privacy During Interruption
Stop recording indicator when recording stops/pauses.
127. Typing Reliability
Missing stop message solved by TTL.
128. Typing Delay
May wait small threshold before advertising to avoid flicker:
~300–500 ms
optional.
129. Typing Keepalive
Low frequency.
130. Battery Sensitivity
Presence/typing updates should be lightweight.
131. Offline Queue
Do not queue stale typing indicators for later delivery.
Hard rule.
132. Do Not Store Typing Durably
Except maybe transient in-memory diagnostics.
133. Do Not Backup Presence
Part 33 excludes ephemeral state.
134. Do Not Search Presence
Part 11 indexes durable content, not typing/presence.
135. Receipt Durability
Read/delivery cursors are durable/syncable state.
136. Presence Durability
Usually not.
Last-seen may be retained according to privacy policy.
137. Event Model
#![allow(unused)] fn main() { pub enum PresenceUiEvent { PresenceChanged { account: AccountId, presence: PresenceSummary, }, AvailabilityChanged { account: AccountId, availability: AvailabilitySummary, }, TypingChanged { conversation: ConversationId, typing: TypingSummary, }, ReceiptChanged(ReceiptUpdateView), } }
138. Presence Summary
#![allow(unused)] fn main() { pub struct PresenceSummary { pub state: PresenceState, pub last_seen: Option<LastSeenView>, } }
139. Availability Summary
#![allow(unused)] fn main() { pub struct AvailabilitySummary { pub state: AvailabilityState, pub custom_status: Option<StatusTextView>, } }
140. Typing Participant View
#![allow(unused)] fn main() { pub struct TypingParticipantView { pub account: AccountId, pub kind: TypingKind, } }
141. Typing Kind
#![allow(unused)] fn main() { pub enum TypingKind { Text, VoiceRecording, } }
142. Read Receipt Presentation
#![allow(unused)] fn main() { pub struct MessageReceiptView { pub delivery: MessageDeliveryState, pub delivered_at: Option<Timestamp>, pub read_at: Option<Timestamp>, } }
143. Group Receipt Summary
#![allow(unused)] fn main() { pub struct GroupReceiptSummaryView { pub delivered_count: u32, pub read_count: u32, pub total_eligible: u32, } }
144. Presence Presentation API
#![allow(unused)] fn main() { pub trait PresencePresentation { async fn contact_presence( &self, account: AccountId, ) -> Result<PresenceSummary, UiError>; async fn visible_contacts( &self, accounts: Vec<AccountId>, ) -> Result<Vec<PresenceSummary>, UiError>; async fn set_availability( &self, availability: AvailabilityState, ) -> Result<(), UiError>; } }
145. Typing Presentation API
#![allow(unused)] fn main() { pub trait TypingPresentation { async fn set_typing( &self, conversation: ConversationId, kind: TypingKind, active: bool, ) -> Result<(), UiError>; } }
146. Receipt Presentation API
#![allow(unused)] fn main() { pub trait ReceiptPresentation { async fn mark_read_through( &self, conversation: ConversationId, sequence: MessageSequence, ) -> Result<(), UiError>; async fn message_receipt( &self, message: MessageId, ) -> Result<MessageReceiptView, UiError>; async fn group_receipts( &self, message: MessageId, ) -> Result<GroupReceiptSummaryView, UiError>; } }
147. Privacy Settings API
#![allow(unused)] fn main() { pub trait PresencePrivacyPresentation { async fn settings( &self, ) -> Result<PresencePrivacySettingsView, UiError>; async fn update( &self, update: PresencePrivacyUpdate, ) -> Result<(), UiError>; } }
148. Presence Privacy Settings
#![allow(unused)] fn main() { pub struct PresencePrivacySettingsView { pub share_online_status: bool, pub share_last_seen: bool, pub share_typing: bool, pub share_read_receipts: bool, } }
149. Android ViewModel
Owns:
visible screen state
status picker
privacy settings presentation
read-visibility reporting
Rust owns:
presence truth
TTL
aggregation
receipt cursors
150. Dioxus Presenter
Owns:
status menu
visible-row subscription hints
focus/visibility reporting
receipt details dialog
151. Visible Contact Optimization
UI may report:
currently visible AccountIds
so Rust/network can prioritize presence updates.
152. Do Not Create One Network Subscription Per Row
Aggregate.
153. Conversation Visibility Reporting
UI sends:
conversation active
visible message range
focus/lifecycle state
to read-detection logic.
154. Read Visibility Event
Potential:
#![allow(unused)] fn main() { pub struct ConversationViewportView { pub conversation: ConversationId, pub first_visible: MessageSequence, pub last_visible: MessageSequence, pub focused: bool, } }
155. Rust Decides Read Advancement
UI provides observations.
Rust applies policy.
156. Why This Matters
Prevents Dioxus and Android from implementing subtly different receipt semantics.
157. Status Formatting
UI localizes:
Online
Away
Busy
Do not disturb
Last seen recently
158. Timestamp Formatting
UI uses local locale/timezone.
159. Presence Does Not Reorder Lists
Hard rule.
160. Typing Does Not Reorder Lists
Hard rule.
161. Read Receipt Does Not Reorder Timeline
Hard rule.
162. Accessibility — Presence
Screen reader:
Alice, online
or:
Alice, status unavailable
163. Typing Accessibility
Can announce:
Alice is typing
carefully.
Avoid repeated announcements every keepalive.
164. Recording Accessibility
Alice is recording a voice message
165. Receipt Accessibility
Outgoing message:
Delivered
Read
Failed
166. Group Receipt Accessibility
Read by 3 of 5
167. Status Picker Accessibility
All states explicitly labeled.
168. Color Independence
Presence dots/status icons always paired with accessible text/labels.
169. Large Font
Header/status text wraps/truncates safely.
170. RTL
Status/typing strings localized and laid out correctly.
171. Reduced Motion
Typing animation can use static text.
172. Screen Reader Live Regions
Use only for meaningful changes.
Do not announce presence flapping.
173. Privacy Test Matrix
Verify:
typing off
read receipts off
presence off
last seen off
invisible
blocked contact
message request
174. Presence Test Matrix
online
away
unknown
stale
multi-device
LAN-only
Internet offline
device sleep
175. Typing Test Matrix
start
keepalive
stop
TTL expiry
group aggregation
background
crash
privacy off
176. Receipt Test Matrix
queued
sent
delivered
read
failed
other-device read
group aggregate
read disabled
notification tap
mark unread
177. Android Tests
Verify:
Activity resumed/paused
screen rotation
process death
background
large font
TalkBack
typing stop on background
read viewport semantics
178. Desktop Tests
Verify:
window focused
unfocused
hidden
multi-window
visible-row presence
keyboard accessibility
179. Multi-Window Read
If same conversation open in two desktop windows:
account/device read state
should not double-send harmful events.
180. Multi-Device Typing Test
Typing from phone and desktop aggregates to one user state.
181. Presence Flap Test
Short path changes do not flicker status due to hysteresis.
182. Stale Last-Seen Test
Wording coarsens as confidence decreases.
183. Large Group Test
Thousands of members do not create thousands of live UI subscriptions.
184. Burst Receipt Test
Many receipt updates coalesce.
185. Performance
Presence updates should be low frequency.
Typing updates sparse.
Read cursors aggregated.
186. Battery
Avoid continuous background presence chatter.
Use adaptive cadence.
187. Network
Presence is lower priority than:
calls
message delivery
security
emergency traffic
188. Emergency Mode
Presence may be disabled/deprioritized to preserve bandwidth.
189. Diagnostics
Advanced view may show:
presence age
last update source
receipt cursor
typing TTL
with privacy-safe identifiers.
190. No Raw Transport Data in Normal UX
No:
gossip heartbeat 9s
BLE presence packet
191. Telemetry
Do not log:
who is online
who is typing
who read which message
as product analytics by default.
192. Safe Metrics
Possible:
presence event rate
typing event rate
receipt propagation latency
stale-expiry count
without identities/content.
193. Crash Reports
Redact contact IDs where possible.
194. Initial Production Scope
Ship:
Online/Away/Unknown
optional last seen
typing
voice-recording indicator
queued/sent/delivered/read/failed
read cursor
presence/read/typing privacy settings
account-level multi-device aggregation
small-group typing aggregation
large-group receipt aggregation
Defer:
rich custom status
per-contact privacy matrix
presence location
device-specific presence display
activity/game/music status
unless needed.
195. Definition of Done
UI/UX Part 14 is complete when:
- presence uses Online/Away/Unknown rather than pretending unknown means offline
- account-level presence aggregates multiple devices in Rust
- availability is distinct from presence
- last-seen privacy and precision are explicit
- typing/recording indicators are ephemeral and TTL-controlled by Rust
- typing/presence never reorder inbox/timeline
- read receipts use durable sequence/cursor semantics
- opening a conversation alone does not automatically mark everything read
- notification display/dismissal does not imply read
- mark-unread remains a local reminder rather than reversing remote receipts
- delivery states are semantically defined
- absence of read receipt is not presented as proof of unread
- requests/blocked/privacy modes suppress ephemeral sharing appropriately
- large groups use aggregation rather than per-member live UI
- Android/desktop viewport/focus observations feed one shared Rust read policy
- accessibility, RTL, large font, reduced motion, and live-region rules are defined
- Rust presence, typing, receipt, and privacy presentation APIs are specified
- multi-device, TTL, offline, lifecycle, privacy, flap, and scale tests are included
196. Final Architecture
RUST EPHEMERAL STATE
│
┌──────────────────┼──────────────────┐
│ │ │
Presence Typing Receipts
│ │ │
TTL/Hysteresis TTL/Rate Limit Durable Cursor
│ │ │
└──────────────────┼──────────────────┘
│
Presence Presentation
┌─────┴─────┐
│ │
Dioxus Compose
│ │
Desktop UX Android UX
UI observations flow back only as:
focus
lifecycle
visible message range
visible contact set
Rust remains responsible for deciding:
presence
typing expiry
read advancement
receipt semantics
197. Final Principle
Presence and receipts should feel informative, not invasive or falsely precise.
The correct model is:
useful hints
+
clear uncertainty
+
durable cursor semantics
+
privacy controls
+
bounded ephemeral updates
not:
every device heartbeat becomes user-visible truth
This keeps the experience calm and trustworthy across Dioxus desktop and Android Compose while Rust remains authoritative for ephemeral state aggregation and receipt semantics.