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.

Two prior code-review rounds were addressed in this commit:

Round 1 (independence):
- Added immutable snapshot columns (title/content/color) on note_recipients.
- Dropped the FK from 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 (validation REJECT fixes):
- 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 (shared handler).
- Added OpenAPI ops for /notes/sent, /notes/received, /notes/{id},
  /notes/{id}/send, /notes/{id}/read, /notes/{id}/archive,
  /notes/{id}/reply, and ran orval codegen.
- use-notifications-socket.ts now shows bilingual toasts for note_received
  and note_replied (suppressed during the socket warmup window).
- home.tsx renders an unread badge on the Notes app tile, fed by
  useReceivedNotes(false) filtered to status === "unread"; refreshes
  automatically on socket invalidation.

Tests: 7 backend tests in notes-share.test.mjs (independence,
archived-reply, owner-reply preserves recipient status, GET /notes/:id
alias, etc.) + the notes-inbox e2e all pass. tx-os typecheck is clean.

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:01:18 +00:00
parent 6352cbf844
commit 65c5a172d6
8 changed files with 1102 additions and 37 deletions
+155
View File
@@ -2732,6 +2732,161 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
# ----- Notes (in-app messaging) -----
/notes/sent:
get:
operationId: listSentNotes
tags: [notes]
summary: List notes the caller has sent
responses:
"200":
description: Sent notes with per-recipient status
content:
application/json:
schema:
type: array
items: { type: object, additionalProperties: true }
/notes/received:
get:
operationId: listReceivedNotes
tags: [notes]
summary: List notes sent to the caller
parameters:
- in: query
name: archived
required: false
schema: { type: boolean, default: false }
responses:
"200":
description: Received notes with sender + status
content:
application/json:
schema:
type: array
items: { type: object, additionalProperties: true }
/notes/{id}:
get:
operationId: getNoteDetail
tags: [notes]
summary: Get a note's full thread (note + recipients + replies)
parameters:
- in: path
name: id
required: true
schema: { type: integer }
responses:
"200":
description: Note thread, scoped to caller (sender, recipient, or admin)
content:
application/json:
schema: { type: object, additionalProperties: true }
"403":
description: Not a participant
content:
application/json:
schema: { $ref: "#/components/schemas/ErrorResponse" }
"404":
description: Note not found
content:
application/json:
schema: { $ref: "#/components/schemas/ErrorResponse" }
/notes/{id}/send:
post:
operationId: sendNote
tags: [notes]
summary: Owner sends an existing note to a list of recipients
parameters:
- in: path
name: id
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [recipientUserIds]
properties:
recipientUserIds:
type: array
items: { type: integer }
responses:
"200":
description: Sent (idempotent on existing recipients)
content:
application/json:
schema: { type: object, additionalProperties: true }
/notes/{id}/read:
post:
operationId: markNoteRead
tags: [notes]
summary: Recipient marks their copy of a note read
parameters:
- in: path
name: id
required: true
schema: { type: integer }
responses:
"200":
description: Marked read
content:
application/json:
schema: { type: object, additionalProperties: true }
/notes/{id}/archive:
post:
operationId: archiveReceivedNote
tags: [notes]
summary: Recipient archives or unarchives their copy of a note
parameters:
- in: path
name: id
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [archived]
properties:
archived: { type: boolean }
responses:
"200":
description: Archive state updated
content:
application/json:
schema: { type: object, additionalProperties: true }
/notes/{id}/reply:
post:
operationId: replyToNote
tags: [notes]
summary: Sender or recipient adds a reply to a note's thread
parameters:
- in: path
name: id
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [content]
properties:
content: { type: string }
recipientUserId:
type: integer
description: Required when the note owner replies and there is more than one recipient.
responses:
"201":
description: Reply created
content:
application/json:
schema: { type: object, additionalProperties: true }
/admin/users/{id}/dependents/notes:
get:
operationId: getAdminUserDependentNotes