Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
119 lines
3.6 KiB
TypeScript
119 lines
3.6 KiB
TypeScript
import { broadcastToUser } from "@/lib/realtime/bus";
|
|
|
|
type PayloadType = Awaited<ReturnType<typeof import("payload").getPayload>>;
|
|
|
|
export interface NotifyInput {
|
|
userId: number;
|
|
type?: string;
|
|
title: string;
|
|
message?: string;
|
|
link?: string;
|
|
/** Skip both mute suppression checks. Used for operationally critical notifications (assignment transfers). */
|
|
bypassMute?: boolean;
|
|
}
|
|
|
|
/** Create an in-app notification for a user. Fire-and-forget: failures are logged, not thrown. */
|
|
export async function notifyUser(payload: PayloadType, input: NotifyInput): Promise<void> {
|
|
try {
|
|
const target = await payload.findByID({
|
|
collection: "users",
|
|
id: input.userId,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
});
|
|
const prefs = (
|
|
target as {
|
|
preferences?: {
|
|
notifications?: { mutedAll?: boolean | null; mutedTypes?: (string | null)[] | null };
|
|
};
|
|
}
|
|
).preferences?.notifications;
|
|
if (!input.bypassMute) {
|
|
if (prefs?.mutedAll) return;
|
|
if (input.type && prefs?.mutedTypes?.includes(input.type)) return;
|
|
}
|
|
|
|
await payload.create({
|
|
collection: "user-notifications",
|
|
data: {
|
|
user: input.userId,
|
|
type: input.type,
|
|
title: input.title,
|
|
message: input.message,
|
|
link: input.link,
|
|
read: false,
|
|
},
|
|
overrideAccess: true,
|
|
depth: 0,
|
|
});
|
|
broadcastToUser("notification", input.userId, { type: input.type ?? null });
|
|
} catch (err) {
|
|
console.error("[Notifications] Failed to create notification:", err);
|
|
}
|
|
}
|
|
|
|
/** Short display label for a notification type, or the raw type when unknown. */
|
|
export function notificationLabel(type?: string | null): string {
|
|
switch (type) {
|
|
case "poker:force-ended":
|
|
return "Poker game ended";
|
|
case "market:offer":
|
|
return "New offer";
|
|
case "market:counter":
|
|
return "Counteroffer";
|
|
case "market:accept":
|
|
return "Offer accepted";
|
|
case "market:reject":
|
|
return "Offer rejected";
|
|
case "market:withdrawn":
|
|
return "Offer withdrawn";
|
|
case "market:closed":
|
|
return "Negotiations closed";
|
|
case "market:sold":
|
|
return "Listing sold";
|
|
case "market:expired":
|
|
return "Listing expired";
|
|
case "award:granted":
|
|
return "Award received";
|
|
case "account:discord-request":
|
|
return "Discord request";
|
|
case "finance:deposit":
|
|
return "Bank deposit";
|
|
case "finance:withdraw":
|
|
return "Bank withdrawal";
|
|
case "finance:transfer":
|
|
return "Bank transfer";
|
|
case "shipment:completed":
|
|
return "Shipment completed";
|
|
case "ticket:created":
|
|
return "New ticket";
|
|
case "ticket:assigned":
|
|
return "Ticket assigned";
|
|
case "ticket:reply":
|
|
return "New reply";
|
|
case "ticket:status":
|
|
return "Ticket updated";
|
|
case "mission:ready-reminder":
|
|
return "Ready reminder";
|
|
case "assignment-transfer:request":
|
|
return "Transfer request";
|
|
case "assignment-transfer:approved":
|
|
return "Transfer approved";
|
|
case "assignment-transfer:rejected":
|
|
return "Transfer rejected";
|
|
case "assignment-transfer:escalated":
|
|
return "Transfer escalated";
|
|
case "assignment-transfer:resolved":
|
|
return "Transfer resolved";
|
|
case "assignment-transfer:completed":
|
|
return "Transfer completed";
|
|
case "assignment-transfer:cancelled":
|
|
return "Transfer cancelled";
|
|
case "award:ribbon-approved":
|
|
return "Ribbon approved";
|
|
case "award:ribbon-rejected":
|
|
return "Ribbon not approved";
|
|
default:
|
|
return type ?? "Notification";
|
|
}
|
|
}
|