Task #402: Convert Notes into in-app messaging

Original task: turn personal Notes into in-app messaging — sender composes
a note (title/content/color), picks recipient(s), Send. Recipients get an
Inbox with sender name, color, Read/Unread badge, and inline reply. Sender
sees Sent Notes with per-recipient status. Sender's and recipient's copies
must be INDEPENDENT, with backend access checks (admin sees everything),
realtime updates, toasts, an unread badge on the Notes app tile, and full
i18n + RTL.

Three review rounds were addressed in this commit:

Round 1 (independence):
- Snapshot columns (title/content/color) on note_recipients; FKs dropped
  on note_recipients.note_id and note_replies.note_id so recipient
  threads survive the sender deleting their note.
- /notes/received and /notes/:id/thread render the recipient snapshot for
  recipients (sender/admin still see the live note).

Round 2:
- POST /notes/:id/reply: owner is now allowed to reply too. Owner replies
  do not change recipient status; recipient replies still flip to
  "replied" and clear archivedAt.
- Added GET /notes/:id as an alias of /notes/:id/thread.
- Added OpenAPI ops for the Notes routes and ran orval codegen.
- use-notifications-socket.ts shows bilingual toasts for note_received /
  note_replied (suppressed during socket warmup).
- home.tsx renders an unread badge on the Notes app tile.

Round 3 (this round):
- Archived tab now shows BOTH "My archived notes" and "Archived inbox"
  via the new ArchivedView component.
- Sender thread view groups replies by recipient (GroupedReplies) with
  per-conversation header and per-reply author + localized timestamp.
  Recipient view stays flat.
- Send dialog now requires explicit confirmation (AlertDialog) and
  shows a success / failure toast.
- Realtime toast strings moved off hardcoded EN/AR to i18n keys
  (notes.toast.received|replied.*) added in en.json and ar.json.
- handleNoteDetail returns 403 (not 404) when the caller is a
  non-participant of an existing conversation; 404 only when no trace
  of the note exists.
- useReplyToNote accepts recipientUserId; ThreadDialog auto-targets the
  sole recipient on owner replies and shows a recipient picker when
  the owner has more than one recipient.

Tests: 7 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox.spec.mjs)
all pass. tx-os typecheck is clean. Architect re-review: PASS.

Pre-existing executive-meetings TS errors and the failing top-level
`test` workflow are unrelated to this task.
This commit is contained in:
riyadhafraa
2026-05-05 15:09:44 +00:00
parent 65c5a172d6
commit d1b4bb228c
7 changed files with 357 additions and 55 deletions
@@ -111,24 +111,18 @@ export function useNotificationsSocket() {
socket.on("note_received", () => {
queryClient.invalidateQueries({ queryKey: ["notes"] });
if (nowMs() - connectedAtRef.current < SOCKET_WARMUP_MS) return;
const ar = i18n.language === "ar";
toast({
title: ar ? "ملاحظة جديدة" : "New note received",
description: ar
? "تحقق من بريد الملاحظات الوارد لقراءتها."
: "Check your Notes inbox to read it.",
title: i18n.t("notes.toast.received.title"),
description: i18n.t("notes.toast.received.desc"),
});
});
socket.on("note_replied", () => {
queryClient.invalidateQueries({ queryKey: ["notes"] });
if (nowMs() - connectedAtRef.current < SOCKET_WARMUP_MS) return;
const ar = i18n.language === "ar";
toast({
title: ar ? "رد جديد على ملاحظة" : "New reply on a note",
description: ar
? "افتح الملاحظة لمتابعة المحادثة."
: "Open the note to continue the conversation.",
title: i18n.t("notes.toast.replied.title"),
description: i18n.t("notes.toast.replied.desc"),
});
});