Skip to content

fix(messages): always show outbound status + long-press copy#104

Merged
epicexcelsior merged 2 commits into
anonmesh:stagingfrom
epicexcelsior:night3/message-ux
Jun 13, 2026
Merged

fix(messages): always show outbound status + long-press copy#104
epicexcelsior merged 2 commits into
anonmesh:stagingfrom
epicexcelsior:night3/message-ux

Conversation

@epicexcelsior

Copy link
Copy Markdown
Collaborator

What

Outbound messages always reflect an honest status now, and message text is copyable.

  • Root cause: status was only assigned after send() resolved, so a slow or offline send showed nothing. The bubble is set to queued before the await, then transitions queued → sent → delivered, or → failed on throw — no silent sends.
  • Long-press a message bubble to copy its text.

Scope

MessagesScreen send-state machine + message bubble.

Test

tsc clean · tier0 config/services clean · fake-money + lint clean. Verified: terminal states are guarded, a messageQueued event with no route keeps the bubble honestly queued off-grid, and a send throw maps to failed (not a swallowed error).

Root cause of the silent status: a state only existed for an outbound
message after 'await send()' resolved — getSendState(msgId) had no
idToSeq mapping until then, so MessageBubble rendered no status row at
all. With no route to the peer the native send promise can stall
indefinitely (known send-hang), so the offline-sent bubble stayed bare
forever; sendMsg/handleMedia also had no catch, so a bridge throw left
the same silent bubble plus an unhandled rejection.

Wire the existing states through the whole lifecycle instead:

- beginOutbound registers a pseudo-seq (negative msgId, the trick the
  failed path already used) as 'queued' BEFORE awaiting the native send,
  so every outbound bubble shows clock/'queued' from the moment it
  exists and the queued banner counts it immediately.
- The pseudo entry feeds seqQueuedAt, so a send whose promise never
  settles flips to 'stale' ("Waiting for peer…") after 45s — the honest
  fallback when the data layer reports nothing.
- adoptSeq migrates the bookkeeping to the real seq once native accepts,
  without clobbering a state a native event already set; the existing
  2s presume-'sent' timer and messageQueued/Delivered/Failed + DB-ack
  reconciliation flows are unchanged on top of it.
- send() is wrapped in try/catch on all three paths (text/media/grid,
  mirroring the existing QA-05 guard) → 'failed', and the no-peer /
  node-down early returns now mark the bubble 'failed' too.
- resolveSeq refuses to downgrade terminal delivered/failed to
  sent/queued, and the stale sweep only flips entries still 'queued',
  so late timers can never roll back a confirmed outcome.

No new states invented: queued/sent/delivered/failed/stale and their
copy already existed in MessageBubble — they were just never assigned
on this path.
Long-pressing a bubble previously did nothing. The bubble container is
now a Pressable: long-press (350ms, matching RecipientPicker) fires a
light haptic via the design-system haptics helper, copies the message
text with expo-clipboard (already a dependency — no new deps), and
confirms with the imperative showToast, the documented surface for
transient "copied" feedback. Subtle pressed opacity signals the
affordance; plain taps remain inert.

Files-only bubbles (empty text) don't attach the handler — there is no
text to copy, and the inner FileRow keeps its own tap. No clipboard
auto-clear: that pattern is reserved for sensitive material like group
keys (ChannelShareSheet), not the user's own conversation text.
@epicexcelsior epicexcelsior merged commit 28b2328 into anonmesh:staging Jun 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant