From dcaebb38f5eff710de59f00eb4da2fd11570cb39 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Thu, 7 May 2026 10:32:25 +0000 Subject: [PATCH] notes: re-send fires popup + notification again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- artifacts/api-server/src/routes/notes.ts | 34 ++++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/artifacts/api-server/src/routes/notes.ts b/artifacts/api-server/src/routes/notes.ts index 9f5e3bf8..ab293bbb 100644 --- a/artifacts/api-server/src/routes/notes.ts +++ b/artifacts/api-server/src/routes/notes.ts @@ -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 => { const userId = req.session.userId!; @@ -652,7 +654,13 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise => { } // 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 => { }) .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 => { }); } - res.json({ success: true, sent: inserted.length }); + res.json({ success: true, sent: allRows.length }); }); // ----- Read / Archive (recipient-side) -----