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:
@@ -405,7 +405,19 @@ async function handleNoteDetail(req: import("express").Request, res: import("exp
|
||||
const admin = await isAdmin(userId);
|
||||
const isSender = !!note && note.userId === userId;
|
||||
|
||||
// Authorization contract: any non-participant gets 403 (not 404), as long
|
||||
// as someone is participating in the conversation. Only return 404 when
|
||||
// there is no trace of the note at all (no live row, no recipient rows).
|
||||
if (!note && !recipientRow) {
|
||||
const [otherRow] = await db
|
||||
.select({ id: noteRecipientsTable.id })
|
||||
.from(noteRecipientsTable)
|
||||
.where(eq(noteRecipientsTable.noteId, id.id))
|
||||
.limit(1);
|
||||
if (otherRow) {
|
||||
res.status(403).json({ error: "Forbidden" });
|
||||
return;
|
||||
}
|
||||
res.status(404).json({ error: "Note not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user