Improve notification deep-linking and add Arabic meeting reminders
Enhance notification handling to preserve deep-link parameters, add relatedId and relatedType to notifications for better association, and implement Arabic pluralization for meeting reminders. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 9abb57f5-4f2c-4eed-9fad-ab278885083e Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fKZh36i Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -222,7 +222,7 @@ export async function broadcastExecutiveMeetingNotifications(
|
||||
type: "executive_meeting",
|
||||
relatedId: meetingId,
|
||||
tag: meetingId ? `meeting-${meetingId}-${notificationType}` : `meeting-${notificationType}`,
|
||||
url: "/meetings",
|
||||
url: meetingId ? `/meetings?meeting=${meetingId}` : "/meetings",
|
||||
});
|
||||
}
|
||||
io.emit("executive_meeting_notifications_changed", {
|
||||
|
||||
@@ -43,6 +43,16 @@ function stripHtmlToPlainText(input: string | null | undefined): string {
|
||||
return decoded.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
// Arabic minute pluralisation for the "starts after N minutes" reminder
|
||||
// body. Arabic uses three forms for small counts: 1 -> دقيقة, 2 -> دقيقتين
|
||||
// (dual), 3+ -> دقائق. The scheduler only fires for deltas of 1-5 min, but
|
||||
// the helper stays correct for the dual/plural boundary regardless.
|
||||
function minutesAfterAr(n: number): string {
|
||||
if (n <= 1) return "بعد دقيقة";
|
||||
if (n === 2) return "بعد دقيقتين";
|
||||
return `بعد ${n} دقائق`;
|
||||
}
|
||||
|
||||
function dateKey(d: Date): string {
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
@@ -161,7 +171,7 @@ async function tickOnce(): Promise<void> {
|
||||
stripHtmlToPlainText(meeting.titleEn) ||
|
||||
stripHtmlToPlainText(meeting.titleAr) ||
|
||||
"Meeting";
|
||||
const bodyAr = `${subjectAr} يبدأ خلال ${deltaMin} دقيقة`;
|
||||
const bodyAr = `${subjectAr} يبدأ ${minutesAfterAr(deltaMin)}`;
|
||||
const bodyEn = `${subjectEn} starts in ${deltaMin} min`;
|
||||
|
||||
for (const { userId } of claimed) {
|
||||
@@ -175,7 +185,7 @@ async function tickOnce(): Promise<void> {
|
||||
bodyAr,
|
||||
bodyEn,
|
||||
tag: `em-reminder-${meeting.id}`,
|
||||
url: "/meetings",
|
||||
url: `/meetings?meeting=${meeting.id}`,
|
||||
type: "executive_meeting",
|
||||
relatedId: meeting.id,
|
||||
},
|
||||
|
||||
@@ -1130,6 +1130,8 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise<void> => {
|
||||
bodyAr: `${senderNameAr} أرسل لك ملاحظة`,
|
||||
bodyEn: `${senderName} sent you a note`,
|
||||
type: "note",
|
||||
relatedType: "note",
|
||||
relatedId: note.id,
|
||||
})
|
||||
.returning();
|
||||
await emitToUser(r.recipientUserId, "notification_created", { ...notif, type: "note" });
|
||||
@@ -1143,7 +1145,7 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise<void> => {
|
||||
type: "note",
|
||||
relatedId: note.id,
|
||||
tag: `note-${note.id}`,
|
||||
url: "/notes",
|
||||
url: `/notes?thread=${note.id}`,
|
||||
});
|
||||
await emitToUser(r.recipientUserId, "note_received", {
|
||||
noteId: note.id,
|
||||
@@ -1488,6 +1490,8 @@ router.post("/notes/:id/reply", requireAuth, async (req, res): Promise<void> =>
|
||||
bodyAr: `${replierNameAr} رد على ملاحظة`,
|
||||
bodyEn: `${replierName} replied on a note`,
|
||||
type: "note",
|
||||
relatedType: "note",
|
||||
relatedId: id.id,
|
||||
})
|
||||
.returning();
|
||||
await emitToUser(otherPartyUserId, "notification_created", { ...notif, type: "note" });
|
||||
@@ -1501,7 +1505,7 @@ router.post("/notes/:id/reply", requireAuth, async (req, res): Promise<void> =>
|
||||
type: "note",
|
||||
relatedId: id.id,
|
||||
tag: `note-reply-${reply.id}`,
|
||||
url: "/notes",
|
||||
url: `/notes?thread=${id.id}`,
|
||||
});
|
||||
// Enrich the realtime payload so the recipient's client can render the
|
||||
// floating reply card without an extra round-trip. We truncate the body
|
||||
|
||||
@@ -117,9 +117,19 @@ async function notifyUser(
|
||||
bodyEn: string,
|
||||
orderId: number,
|
||||
actorId?: number,
|
||||
// Which order surface the deep-link should open: the recipient's own
|
||||
// "My orders" list (status/cancel updates) or the "Incoming orders"
|
||||
// queue (a brand-new order to claim). Drives both the stored
|
||||
// notification's relatedType and the Web Push deep-link URL so a tap
|
||||
// lands on the right page with the order highlighted.
|
||||
target: "mine" | "incoming" = "mine",
|
||||
) {
|
||||
// Never notify users about actions they themselves initiated.
|
||||
if (actorId !== undefined && actorId === userId) return;
|
||||
const relatedType = target === "incoming" ? "incoming_order" : "order";
|
||||
const deepLinkUrl = `${
|
||||
target === "incoming" ? "/orders/incoming" : "/my-orders"
|
||||
}?order=${orderId}`;
|
||||
const [notification] = await db
|
||||
.insert(notificationsTable)
|
||||
.values({
|
||||
@@ -130,7 +140,7 @@ async function notifyUser(
|
||||
bodyEn,
|
||||
type: "order",
|
||||
relatedId: orderId,
|
||||
relatedType: "order",
|
||||
relatedType,
|
||||
})
|
||||
.returning();
|
||||
await emitToUser(userId, "notification_created", notification);
|
||||
@@ -154,7 +164,7 @@ async function notifyUser(
|
||||
type: "order",
|
||||
relatedId: orderId,
|
||||
tag: `order-${orderId}`,
|
||||
url: "/my-orders",
|
||||
url: deepLinkUrl,
|
||||
},
|
||||
{ ignoreConnected: true },
|
||||
);
|
||||
@@ -209,6 +219,7 @@ router.post("/orders", requireAuth, async (req, res): Promise<void> => {
|
||||
service.nameEn,
|
||||
order.id,
|
||||
userId,
|
||||
"incoming",
|
||||
);
|
||||
}
|
||||
await broadcastIncomingChanged(receivers);
|
||||
|
||||
Reference in New Issue
Block a user