mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
refactor: further trim PR #458 debloat across patches, notices, Select, music, archive, search
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,27 +1,17 @@
|
||||
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements";
|
||||
|
||||
type RawNotification = Record<string, unknown>;
|
||||
|
||||
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<string, ArchivedNotification>;
|
||||
|
||||
export type ArchivesByUser = Record<string, ArchiveMap>;
|
||||
|
||||
type RawNotification = Record<string, unknown>;
|
||||
|
||||
export async function resolveNotificationUserKey(): Promise<string | null> {
|
||||
try {
|
||||
const info = await getUserInfo();
|
||||
@@ -45,165 +35,58 @@ export async function fetchAllNotifications(): Promise<RawNotification[]> {
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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 },
|
||||
};
|
||||
function archiveTimestamp(
|
||||
item: ArchivedNotification & { timestamp?: string },
|
||||
): number {
|
||||
const ms = new Date(
|
||||
String(item.raw?.timestamp ?? item.timestamp ?? 0),
|
||||
).getTime();
|
||||
return Number.isNaN(ms) ? 0 : ms;
|
||||
}
|
||||
|
||||
export function mergeNotificationsIntoArchive(
|
||||
existing: ArchiveMap,
|
||||
notifications: RawNotification[],
|
||||
): ArchiveMap {
|
||||
): { archive: ArchiveMap; changed: boolean } {
|
||||
const now = new Date().toISOString();
|
||||
const merged: ArchiveMap = { ...existing };
|
||||
let changed = false;
|
||||
const archive = { ...existing };
|
||||
|
||||
for (const raw of notifications) {
|
||||
const normalized = normalizeArchivedNotification(raw, now);
|
||||
if (!normalized) continue;
|
||||
const notificationID = Number(raw.notificationID);
|
||||
if (!notificationID || Number.isNaN(notificationID)) 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;
|
||||
}
|
||||
const key = String(notificationID);
|
||||
const prev = archive[key];
|
||||
archive[key] = prev
|
||||
? { ...prev, lastSeenAt: now, raw: { ...prev.raw, ...raw } }
|
||||
: { notificationID, firstSavedAt: now, lastSeenAt: now, raw: { ...raw } };
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return merged;
|
||||
return { archive, changed };
|
||||
}
|
||||
|
||||
export function listArchivedNotifications(archive: ArchiveMap): ArchivedNotification[] {
|
||||
export function listArchivedNotifications(
|
||||
archive: ArchiveMap,
|
||||
): ArchivedNotification[] {
|
||||
return Object.values(archive).sort(
|
||||
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
|
||||
(a, b) => archiveTimestamp(b) - archiveTimestamp(a),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
return { ...item.raw, notificationID: item.notificationID };
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
} from "./injectArchivedNotifications";
|
||||
import styles from "./styles.css?inline";
|
||||
|
||||
const BUBBLE_SELECTOR = "[class*='notifications__bubble___']";
|
||||
const LIST_SELECTOR = '[class*="notifications__list___"]';
|
||||
|
||||
const notificationCollectorSettings = {
|
||||
saveLocally: booleanSetting({
|
||||
default: true,
|
||||
@@ -60,15 +63,9 @@ const notificationCollectorPlugin: Plugin<
|
||||
const baseInterval = 30000;
|
||||
const maxInterval = 300000;
|
||||
|
||||
if (!api.storage.lastNotificationCount) {
|
||||
api.storage.lastNotificationCount = 0;
|
||||
}
|
||||
if (!api.storage.consecutiveErrors) {
|
||||
api.storage.consecutiveErrors = 0;
|
||||
}
|
||||
if (!api.storage.archivesByUser) {
|
||||
api.storage.archivesByUser = {};
|
||||
}
|
||||
api.storage.lastNotificationCount ||= 0;
|
||||
api.storage.consecutiveErrors ||= 0;
|
||||
api.storage.archivesByUser ||= {};
|
||||
|
||||
const syncArchive = async () => {
|
||||
if (!api.settings.saveLocally || archiveSyncInFlight) return;
|
||||
@@ -81,12 +78,15 @@ const notificationCollectorPlugin: Plugin<
|
||||
const notifications = await fetchAllNotifications();
|
||||
const archivesByUser = { ...(api.storage.archivesByUser ?? {}) };
|
||||
const existing = archivesByUser[userKey] ?? {};
|
||||
const merged = mergeNotificationsIntoArchive(existing, notifications);
|
||||
const { archive: merged, changed } = mergeNotificationsIntoArchive(
|
||||
existing,
|
||||
notifications,
|
||||
);
|
||||
|
||||
if (JSON.stringify(existing) !== JSON.stringify(merged)) {
|
||||
if (changed) {
|
||||
archivesByUser[userKey] = merged;
|
||||
api.storage.archivesByUser = archivesByUser;
|
||||
} else if (document.querySelector('[class*="notifications__list___"]')) {
|
||||
} else if (document.querySelector(LIST_SELECTOR)) {
|
||||
await injectArchivedForUser(merged);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -100,9 +100,7 @@ const notificationCollectorPlugin: Plugin<
|
||||
if (!isVisible) return;
|
||||
|
||||
try {
|
||||
const alertDiv = document.querySelector(
|
||||
"[class*='notifications__bubble___']",
|
||||
) as HTMLElement;
|
||||
const alertDiv = document.querySelector(BUBBLE_SELECTOR) as HTMLElement;
|
||||
|
||||
if (alertDiv && api.storage.lastNotificationCount !== 0) {
|
||||
alertDiv.textContent = api.storage.lastNotificationCount.toString();
|
||||
@@ -159,9 +157,7 @@ const notificationCollectorPlugin: Plugin<
|
||||
if (pollInterval) {
|
||||
window.clearTimeout(pollInterval);
|
||||
pollInterval = null;
|
||||
const alertDiv = document.querySelector(
|
||||
"[class*='notifications__bubble___']",
|
||||
) as HTMLElement;
|
||||
const alertDiv = document.querySelector(BUBBLE_SELECTOR) as HTMLElement;
|
||||
if (alertDiv) {
|
||||
if (api.storage.lastNotificationCount > 9) {
|
||||
alertDiv.textContent = "9+";
|
||||
@@ -175,10 +171,7 @@ const notificationCollectorPlugin: Plugin<
|
||||
const handleVisibilityChange = () => {
|
||||
isVisible = !document.hidden;
|
||||
if (isVisible && !pollInterval) {
|
||||
const alertDiv = document.querySelector(
|
||||
"[class*='notifications__bubble___']",
|
||||
);
|
||||
if (alertDiv) startPolling();
|
||||
if (document.querySelector(BUBBLE_SELECTOR)) startPolling();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -195,18 +188,16 @@ const notificationCollectorPlugin: Plugin<
|
||||
resolveNotificationUserKey,
|
||||
);
|
||||
|
||||
api.seqta.onMount("[class*='notifications__bubble___']", () => {
|
||||
const onBubbleMount = () => {
|
||||
startPolling();
|
||||
if (api.settings.saveLocally) {
|
||||
void syncArchive();
|
||||
}
|
||||
});
|
||||
if (api.settings.saveLocally) void syncArchive();
|
||||
};
|
||||
const onListMount = () => {
|
||||
if (api.settings.saveLocally) void syncArchive();
|
||||
};
|
||||
|
||||
api.seqta.onMount("[class*='notifications__list___']", () => {
|
||||
if (api.settings.saveLocally) {
|
||||
void syncArchive();
|
||||
}
|
||||
});
|
||||
api.seqta.onMount(BUBBLE_SELECTOR, onBubbleMount);
|
||||
api.seqta.onMount(LIST_SELECTOR, onListMount);
|
||||
|
||||
return () => {
|
||||
stopPolling();
|
||||
|
||||
@@ -14,9 +14,6 @@ const ITEM_SELECTOR = '[class*="notifications__item___"]';
|
||||
const BACKED_UP_CLASS = "bsplus-notification-backed-up";
|
||||
const BACKUP_BADGE_CLASS = "bsplus-notification-backup-badge";
|
||||
|
||||
const BACKUP_CHECK_SVG =
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>';
|
||||
|
||||
function notificationTimestamp(item: Record<string, unknown>): number {
|
||||
const ms = new Date(String(item.timestamp ?? 0)).getTime();
|
||||
return Number.isNaN(ms) ? 0 : ms;
|
||||
@@ -38,17 +35,6 @@ function mergeLiveWithArchived(
|
||||
);
|
||||
}
|
||||
|
||||
function sameItemOrder(
|
||||
current: Record<string, unknown>[],
|
||||
merged: Record<string, unknown>[],
|
||||
): boolean {
|
||||
if (current.length !== merged.length) return false;
|
||||
return current.every(
|
||||
(item, index) =>
|
||||
Number(item.notificationID) === Number(merged[index]?.notificationID),
|
||||
);
|
||||
}
|
||||
|
||||
async function tryInjectArchived(archive: ArchiveMap): Promise<boolean> {
|
||||
if (!document.querySelector(LIST_SELECTOR)) return false;
|
||||
|
||||
@@ -58,16 +44,22 @@ async function tryInjectArchived(archive: ArchiveMap): Promise<boolean> {
|
||||
const liveItems = state.items as Record<string, unknown>[];
|
||||
const merged = mergeLiveWithArchived(liveItems, archive);
|
||||
if (!merged) return true;
|
||||
if (sameItemOrder(liveItems, merged)) return true;
|
||||
|
||||
const sameOrder =
|
||||
liveItems.length === merged.length &&
|
||||
liveItems.every(
|
||||
(item, index) =>
|
||||
Number(item.notificationID) === Number(merged[index]?.notificationID),
|
||||
);
|
||||
if (sameOrder) return true;
|
||||
|
||||
await ReactFiber.find(LIST_SELECTOR).setState({ items: merged });
|
||||
return true;
|
||||
}
|
||||
|
||||
async function injectWithRetries(archive: ArchiveMap, attempts = 10) {
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
const done = await tryInjectArchived(archive);
|
||||
if (done) break;
|
||||
async function injectWithRetries(archive: ArchiveMap) {
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
if (await tryInjectArchived(archive)) break;
|
||||
await delay(120);
|
||||
}
|
||||
applyBackupBadges(archive);
|
||||
@@ -86,7 +78,7 @@ export function applyBackupBadges(archive: ArchiveMap) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = BACKUP_BADGE_CLASS;
|
||||
badge.title = "Saved locally";
|
||||
badge.innerHTML = BACKUP_CHECK_SVG;
|
||||
badge.textContent = "✓";
|
||||
itemEl.appendChild(badge);
|
||||
}
|
||||
} else {
|
||||
@@ -119,30 +111,23 @@ export function mountArchivedNotificationInjection(
|
||||
const watchItemsContainer = () => {
|
||||
const itemsEl = document.querySelector(ITEMS_SELECTOR);
|
||||
if (!itemsEl) return;
|
||||
if (observer) observer.disconnect();
|
||||
observer = new MutationObserver(() => scheduleInject());
|
||||
observer?.disconnect();
|
||||
observer = new MutationObserver(scheduleInject);
|
||||
observer.observe(itemsEl, { childList: true });
|
||||
};
|
||||
|
||||
api.seqta.onMount(LIST_SELECTOR, () => {
|
||||
const onNotificationsMount = () => {
|
||||
scheduleInject();
|
||||
watchItemsContainer();
|
||||
});
|
||||
|
||||
api.seqta.onMount(ITEMS_SELECTOR, () => {
|
||||
watchItemsContainer();
|
||||
scheduleInject();
|
||||
});
|
||||
|
||||
api.storage.onChange("archivesByUser", () => scheduleInject());
|
||||
|
||||
return () => {
|
||||
if (observer) observer.disconnect();
|
||||
};
|
||||
|
||||
api.seqta.onMount(LIST_SELECTOR, onNotificationsMount);
|
||||
api.seqta.onMount(ITEMS_SELECTOR, onNotificationsMount);
|
||||
api.storage.onChange("archivesByUser", scheduleInject);
|
||||
|
||||
return () => observer?.disconnect();
|
||||
}
|
||||
|
||||
export async function injectArchivedForUser(
|
||||
archive: ArchiveMap,
|
||||
): Promise<void> {
|
||||
export async function injectArchivedForUser(archive: ArchiveMap): Promise<void> {
|
||||
await injectWithRetries(archive);
|
||||
}
|
||||
|
||||
@@ -10,16 +10,11 @@
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--better-main, #22c55e);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.bsplus-notification-backup-badge svg {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user