Task #39: Send a real chat notification when someone messages a non-muted chat
Wired the chat message-send handler in artifacts/api-server/src/routes/conversations.ts to
create rows in the `notifications` table for every conversation participant who:
- is not the sender
- has `is_muted = false` on `conversation_participants`
Implementation details:
- Added a `createMessageNotifications` helper invoked after the new_message socket emit.
- Imported `notificationsTable` from `@workspace/db`.
- Notification content:
* Direct chat: title = sender display name, body = message preview (≤140 chars)
* Group chat: title = group name (fallback chain), body = "{sender}: {preview}"
Both Arabic and English titles/bodies are populated using the sender's
displayNameAr/displayNameEn (with username fallback) so the existing
bilingual notifications page renders correctly.
- type = "chat", relatedType = "conversation", relatedId = conversation id, so
notifications can later be deep-linked to the chat.
Muted conversations are filtered at the SQL level, so no notification rows are
created for muted recipients — the existing `conversation_participants.is_muted`
flag is the single source of truth, satisfying the task acceptance criteria.
No schema changes were needed; existing `notifications` table fields cover this.
The notifications page and bell badge already read from `useListNotifications`,
so no frontend changes were required — entries appear on next refetch/navigation.
Pre-existing TypeScript errors in the api-server (stale codegen for
@workspace/api-zod and unrelated schema fields) are not introduced by this change;
the esbuild build succeeds and the server starts cleanly.
Replit-Task-Id: 6b146a53-a575-4921-bd4f-3332247779df
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
conversationParticipantsTable,
|
||||
messagesTable,
|
||||
messageReadsTable,
|
||||
notificationsTable,
|
||||
usersTable,
|
||||
} from "@workspace/db";
|
||||
import { requireAuth } from "../middlewares/auth";
|
||||
@@ -763,9 +764,89 @@ router.post("/conversations/:id/messages", requireAuth, async (req, res): Promis
|
||||
const { io } = await import("../index.js");
|
||||
io.to(`conversation:${params.data.id}`).emit("new_message", fullMessage);
|
||||
|
||||
await createMessageNotifications({
|
||||
conversationId: params.data.id,
|
||||
senderId: userId,
|
||||
sender,
|
||||
content: parsed.data.content,
|
||||
});
|
||||
|
||||
res.status(201).json(fullMessage);
|
||||
});
|
||||
|
||||
async function createMessageNotifications(input: {
|
||||
conversationId: number;
|
||||
senderId: number;
|
||||
sender: {
|
||||
displayNameAr: string | null;
|
||||
displayNameEn: string | null;
|
||||
username: string;
|
||||
};
|
||||
content: string;
|
||||
}): Promise<void> {
|
||||
const recipients = await db
|
||||
.select({ userId: conversationParticipantsTable.userId })
|
||||
.from(conversationParticipantsTable)
|
||||
.where(
|
||||
and(
|
||||
eq(conversationParticipantsTable.conversationId, input.conversationId),
|
||||
eq(conversationParticipantsTable.isMuted, false),
|
||||
sql`${conversationParticipantsTable.userId} != ${input.senderId}`,
|
||||
),
|
||||
);
|
||||
|
||||
if (recipients.length === 0) return;
|
||||
|
||||
const [conv] = await db
|
||||
.select({
|
||||
isGroup: conversationsTable.isGroup,
|
||||
nameAr: conversationsTable.nameAr,
|
||||
nameEn: conversationsTable.nameEn,
|
||||
})
|
||||
.from(conversationsTable)
|
||||
.where(eq(conversationsTable.id, input.conversationId));
|
||||
if (!conv) return;
|
||||
|
||||
const senderNameAr =
|
||||
input.sender.displayNameAr || input.sender.displayNameEn || input.sender.username;
|
||||
const senderNameEn =
|
||||
input.sender.displayNameEn || input.sender.displayNameAr || input.sender.username;
|
||||
|
||||
const preview = input.content.trim().slice(0, 140);
|
||||
|
||||
let titleAr: string;
|
||||
let titleEn: string;
|
||||
let bodyAr: string | null;
|
||||
let bodyEn: string | null;
|
||||
|
||||
if (conv.isGroup) {
|
||||
const groupNameAr = conv.nameAr || conv.nameEn || senderNameAr;
|
||||
const groupNameEn = conv.nameEn || conv.nameAr || senderNameEn;
|
||||
titleAr = groupNameAr;
|
||||
titleEn = groupNameEn;
|
||||
bodyAr = preview ? `${senderNameAr}: ${preview}` : senderNameAr;
|
||||
bodyEn = preview ? `${senderNameEn}: ${preview}` : senderNameEn;
|
||||
} else {
|
||||
titleAr = senderNameAr;
|
||||
titleEn = senderNameEn;
|
||||
bodyAr = preview || null;
|
||||
bodyEn = preview || null;
|
||||
}
|
||||
|
||||
await db.insert(notificationsTable).values(
|
||||
recipients.map((r) => ({
|
||||
userId: r.userId,
|
||||
titleAr,
|
||||
titleEn,
|
||||
bodyAr,
|
||||
bodyEn,
|
||||
type: "chat",
|
||||
relatedId: input.conversationId,
|
||||
relatedType: "conversation",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
router.post("/conversations/:id/read", requireAuth, async (req, res): Promise<void> => {
|
||||
const params = MarkConversationReadParams.safeParse(req.params);
|
||||
if (!params.success) {
|
||||
|
||||
Reference in New Issue
Block a user