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:
2026-07-19 08:58:31 +09:30
parent 4fda63dedd
commit 3bac45032d
21 changed files with 497 additions and 924 deletions
@@ -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;
}