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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user