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) -----