Task #427: fix floating note popup hanging on touch devices

Root cause on iOS Safari (iPad/phone): the `IncomingNotePopup`
rendered a `fixed inset-0 z-[110]` full-viewport wrapper with
`pointer-events-none`. Stacking that layer on top of the upcoming
meeting alert plus a Radix toast intermittently caused the next
finger tap to be swallowed — the user saw the popup "hang".

Fixes:
- Drop the full-viewport wrapper: render the popup card directly
  as a sized `fixed` element. No more invisible viewport-sized
  layer between touch and the rest of the UI.
- Suppress the redundant note/reply toast when the floating
  popup actually accepts the event. The popup IS the surface;
  doubling up just stacks one more floating layer.
- Remove `autoFocus` on the Reply button. A new popup arriving
  while the user is mid-tap would steal focus, contributing to
  the perceived hang.

Tests:
- New `notes-popup-touch-tap.spec.mjs`: emulates a touch viewport,
  sends a real cross-user note, verifies popup bounding box is
  card-sized (not viewport-sized), the redundant toast is
  suppressed, and a `locator.tap()` on Reply dismisses the
  popup and navigates.
- `tsc --noEmit` clean.

Files:
- artifacts/tx-os/src/components/notes/incoming-note-popup.tsx
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
- artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs (new)
This commit is contained in:
Riyadh
2026-05-06 11:43:23 +00:00
parent 569492f17d
commit 6adf42617f
4 changed files with 254 additions and 34 deletions
@@ -168,10 +168,18 @@ export function useNotificationsSocket() {
vibrate: user.vibrationEnabledNote,
});
}
toast({
title: i18n.t("notes.toast.received.title"),
description: i18n.t("notes.toast.received.desc"),
});
// Only fall back to a toast when the floating popup did NOT
// accept this event (sender's own note, duplicate, malformed,
// etc.). When the popup IS shown, the toast is redundant — and
// on touch devices stacking a toast over an already-stacked
// popup + meeting alert produced extra layers that intermittently
// intercepted the next tap (#427).
if (!enqueued) {
toast({
title: i18n.t("notes.toast.received.title"),
description: i18n.t("notes.toast.received.desc"),
});
}
});
socket.on(
@@ -245,10 +253,14 @@ export function useNotificationsSocket() {
vibrate: user.vibrationEnabledNote,
});
}
toast({
title: i18n.t("notes.toast.replied.title"),
description: i18n.t("notes.toast.replied.desc"),
});
// See `note_received` above: skip the redundant toast when the
// floating popup is the surface (#427).
if (!enqueued) {
toast({
title: i18n.t("notes.toast.replied.title"),
description: i18n.t("notes.toast.replied.desc"),
});
}
},
);