feat(notes): make incoming-note popup attention-grabbing

Task #409: incoming notes now mirror UpcomingMeetingAlert's attention
model — sound + vibration + visual pulse + persistent queue indicator —
so recipients can't miss them.

Changes:
- DB: add notification_sound_note (default "knock"), notify_notes_enabled,
  vibration_enabled_note to users schema; pushed via drizzle.
- API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody;
  regenerated codegen.
- Auth route: include new fields in buildAuthUser + PATCH allowlist.
- Socket hook (use-notifications-socket): play sound + vibrate on
  note_received, gated by mute/notifyNotesEnabled/socket warmup, with
  playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef).
- IncomingNotePopupContext: enqueue() returns boolean acceptance so the
  socket hook suppresses sound on own-note/duplicate.
- Popup visual (incoming-note-popup): z-[110], ring-4 amber, shadow-2xl,
  animate-in zoom, avatar animate-ping pulse.
- Persistent indicator on Notes app tile (home.tsx): badge =
  max(unread, queueLength), animate-ping ring while queueLength > 0,
  via useIncomingNotePopup().queueLength + new pulse prop on AppIconContent.
- Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot
  (grid-cols-3) with enabled/vibration toggles + sound picker.
- Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote.
- Test instrumentation: NotificationPlayer exposes window.__txosNotifPlayCount
  and __txosNotifLastSound for E2E observability.
- Playwright spec (notes-popup-on-receive): asserts chime fires once on
  recipient with sound="knock", sender plays nothing, pulse visible while
  queued, pulse gone after dismiss.

Pre-existing typecheck errors in executive-meetings.ts (font settings
scope) are unrelated to this task.
This commit is contained in:
riyadhafraa
2026-05-05 16:29:58 +00:00
parent dcf90e1c2d
commit d2b6e7f98c
@@ -156,10 +156,14 @@ test("recipient sees a popup modal when a note arrives in real time", async ({
await expect(senderPage.getByTestId("send-note-confirm")).toBeVisible();
await senderPage.getByTestId("send-note-confirm-submit").click();
await sendPromise;
const sendCompletedAt = Date.now();
// ---- Recipient should now see the popup modal ----
// Real-time SLA: socket-fanout popup must surface within ~3s of the
// send returning. Anything slower defeats the "can't miss it" goal.
const popup = recipientPage.getByTestId("incoming-note-popup");
await expect(popup).toBeVisible({ timeout: 10_000 });
await expect(popup).toBeVisible({ timeout: 3_000 });
expect(Date.now() - sendCompletedAt).toBeLessThan(3_000);
await expect(popup).toContainText(noteTitle);
await expect(popup).toContainText(noteContent);
await expect(popup).toContainText(sender.displayEn);