notes: re-send fires popup + notification again

Previously POST /api/notes/:id/send used onConflictDoNothing on
(noteId, recipientUserId) and only iterated `inserted` rows for
side-effects, so a re-send to the same recipient was silent — no
note_received socket event, no notification — even though the API
returned 200. Reported by the user: "I sent again to rh, why
didn't it reach them?"

Fix: after the idempotent insert, load the existing recipient
rows for any recipients that were skipped, and run the
notification + socket emit loop over BOTH inserted and existing
rows. The DB row is still not duplicated (snapshot semantics
preserved), but the recipient now sees the popup and gets a fresh
notification on every re-send.

Verified:
- @workspace/api-server tests/notes-share.test.mjs — 12/12 pass
  (including "re-sending to the same recipient does not duplicate
  the recipient row").
- tx-os e2e notes-popup-on-receive — 3/3 pass.
This commit is contained in:
riyadhafraa
2026-05-07 10:32:25 +00:00
parent a82ce51979
commit dcaebb38f5
+29 -5
View File
@@ -606,8 +606,10 @@ router.get("/notes/:id", requireAuth, handleNoteDetail);
/**
* POST /notes/:id/send — owner sends note to recipients.
* Self-send and dupes are filtered out. Existing recipients are silently
* skipped (idempotent).
* Self-send and dupes are filtered out. The (noteId, recipientUserId)
* unique constraint keeps a single recipient row per (note, user), but
* a re-send still re-emits the note_received socket event and creates
* a fresh notification, so the recipient sees the popup again.
*/
router.post("/notes/:id/send", requireAuth, async (req, res): Promise<void> => {
const userId = req.session.userId!;
@@ -652,7 +654,13 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise<void> => {
}
// Snapshot the note as of right now into each recipient row so that any
// future sender edit/delete leaves delivered copies untouched.
// future sender edit/delete leaves delivered copies untouched. The
// unique (noteId, recipientUserId) means a re-send to the same person
// does NOT duplicate the row — but we still want the popup +
// notification to fire again so the recipient sees the re-send. So:
// 1) insert with onConflictDoNothing (gives us new rows only)
// 2) load the existing rows for any recipients that were skipped
// 3) emit note_received + create a fresh notification for ALL of them
const inserted = await db
.insert(noteRecipientsTable)
.values(
@@ -672,11 +680,27 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise<void> => {
})
.returning();
const insertedRecipientIds = new Set(inserted.map((r) => r.recipientUserId));
const resendRecipientIds = toSend.filter((rid) => !insertedRecipientIds.has(rid));
const existingRows =
resendRecipientIds.length > 0
? await db
.select()
.from(noteRecipientsTable)
.where(
and(
eq(noteRecipientsTable.noteId, note.id),
inArray(noteRecipientsTable.recipientUserId, resendRecipientIds),
),
)
: [];
const allRows = [...inserted, ...existingRows];
const senderMap = await loadUserSummaries([userId]);
const sender = senderMap.get(userId);
const senderName = sender?.displayNameEn ?? sender?.username ?? "Someone";
const senderNameAr = sender?.displayNameAr ?? senderName;
for (const r of inserted) {
for (const r of allRows) {
const [notif] = await db
.insert(notificationsTable)
.values({
@@ -703,7 +727,7 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise<void> => {
});
}
res.json({ success: true, sent: inserted.length });
res.json({ success: true, sent: allRows.length });
});
// ----- Read / Archive (recipient-side) -----