import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements"; export interface ArchivedNotification { notificationID: number; type: string; timestamp: string; title: string; subtitle: string; messageID?: number; assessmentID?: number; programmeID?: number; metaclassID?: number; subjectCode?: string; firstSavedAt: string; lastSeenAt: string; raw: RawNotification; } export type ArchiveMap = Record; export type ArchivesByUser = Record; type RawNotification = Record; export async function resolveNotificationUserKey(): Promise { try { const info = await getUserInfo(); const id = info?.id ?? info?.personUUID ?? info?.username; if (id == null || id === "") return null; const label = info?.displayName ?? info?.name ?? info?.username ?? String(id); return `${location.hostname}:${id}:${String(label).slice(0, 64)}`; } catch { return null; } } export async function fetchAllNotifications(): Promise { const res = await fetch(`${location.origin}/seqta/student/heartbeat?`, { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, credentials: "include", body: JSON.stringify({ timestamp: "1970-01-01 00:00:00.0", hash: "#?page=/notifications", }), }); if (!res.ok) return []; const json = (await res.json()) as { notifications?: RawNotification[]; payload?: { notifications?: RawNotification[] }; }; const list = json.notifications ?? json.payload?.notifications; return Array.isArray(list) ? list : []; } function readString(value: unknown): string { if (value == null) return ""; return String(value).trim(); } export function normalizeArchivedNotification( raw: RawNotification, now = new Date().toISOString(), ): ArchivedNotification | null { const notificationID = Number(raw.notificationID); if (!notificationID || Number.isNaN(notificationID)) return null; const type = readString(raw.type) || "unknown"; const timestamp = readString(raw.timestamp) || now; if (type === "message" && raw.message && typeof raw.message === "object") { const message = raw.message as Record; return { notificationID, type, timestamp, title: readString(message.title) || "Message", subtitle: readString(message.subtitle), messageID: Number(message.messageID) || undefined, firstSavedAt: now, lastSeenAt: now, raw: { ...raw }, }; } if ( type === "coneqtassessments" && raw.coneqtAssessments && typeof raw.coneqtAssessments === "object" ) { const assessment = raw.coneqtAssessments as Record; return { notificationID, type, timestamp, title: readString(assessment.title) || "Assessment", subtitle: readString(assessment.subtitle) || readString(assessment.subjectCode), assessmentID: Number(assessment.assessmentID) || undefined, programmeID: Number(assessment.programmeID) || undefined, metaclassID: Number(assessment.metaclassID) || undefined, subjectCode: readString(assessment.subjectCode) || undefined, firstSavedAt: now, lastSeenAt: now, raw: { ...raw }, }; } return { notificationID, type, timestamp, title: readString(raw.title) || "Notification", subtitle: readString(raw.subtitle), firstSavedAt: now, lastSeenAt: now, raw: { ...raw }, }; } export function mergeNotificationsIntoArchive( existing: ArchiveMap, notifications: RawNotification[], ): ArchiveMap { const now = new Date().toISOString(); const merged: ArchiveMap = { ...existing }; for (const raw of notifications) { const normalized = normalizeArchivedNotification(raw, now); if (!normalized) continue; const key = String(normalized.notificationID); const prev = merged[key]; if (prev) { merged[key] = { ...prev, ...normalized, timestamp: normalized.timestamp || prev.timestamp, firstSavedAt: prev.firstSavedAt, lastSeenAt: now, raw: { ...prev.raw, ...raw }, }; } else { merged[key] = normalized; } } return merged; } export function listArchivedNotifications(archive: ArchiveMap): ArchivedNotification[] { return Object.values(archive).sort( (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), ); } export function archivedToApiNotification( item: ArchivedNotification, ): RawNotification { if (item.raw && typeof item.raw === "object") { return { ...item.raw, notificationID: item.notificationID, type: item.type, timestamp: item.timestamp, }; } if (item.type === "message") { return { notificationID: item.notificationID, type: "message", timestamp: item.timestamp, message: { title: item.title, subtitle: item.subtitle, messageID: item.messageID, }, }; } if (item.type === "coneqtassessments") { return { notificationID: item.notificationID, type: "coneqtassessments", timestamp: item.timestamp, coneqtAssessments: { title: item.title, subtitle: item.subtitle, assessmentID: item.assessmentID, programmeID: item.programmeID, metaclassID: item.metaclassID, subjectCode: item.subjectCode, term: "", }, }; } return { notificationID: item.notificationID, type: item.type, timestamp: item.timestamp, title: item.title, subtitle: item.subtitle, }; }