mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
feat(notifications): local per-user archive
This commit is contained in:
@@ -13,6 +13,7 @@ export interface ArchivedNotification {
|
|||||||
subjectCode?: string;
|
subjectCode?: string;
|
||||||
firstSavedAt: string;
|
firstSavedAt: string;
|
||||||
lastSeenAt: string;
|
lastSeenAt: string;
|
||||||
|
raw: RawNotification;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ArchiveMap = Record<string, ArchivedNotification>;
|
export type ArchiveMap = Record<string, ArchivedNotification>;
|
||||||
@@ -82,6 +83,7 @@ export function normalizeArchivedNotification(
|
|||||||
messageID: Number(message.messageID) || undefined,
|
messageID: Number(message.messageID) || undefined,
|
||||||
firstSavedAt: now,
|
firstSavedAt: now,
|
||||||
lastSeenAt: now,
|
lastSeenAt: now,
|
||||||
|
raw: { ...raw },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +105,7 @@ export function normalizeArchivedNotification(
|
|||||||
subjectCode: readString(assessment.subjectCode) || undefined,
|
subjectCode: readString(assessment.subjectCode) || undefined,
|
||||||
firstSavedAt: now,
|
firstSavedAt: now,
|
||||||
lastSeenAt: now,
|
lastSeenAt: now,
|
||||||
|
raw: { ...raw },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +117,7 @@ export function normalizeArchivedNotification(
|
|||||||
subtitle: readString(raw.subtitle),
|
subtitle: readString(raw.subtitle),
|
||||||
firstSavedAt: now,
|
firstSavedAt: now,
|
||||||
lastSeenAt: now,
|
lastSeenAt: now,
|
||||||
|
raw: { ...raw },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +138,10 @@ export function mergeNotificationsIntoArchive(
|
|||||||
merged[key] = {
|
merged[key] = {
|
||||||
...prev,
|
...prev,
|
||||||
...normalized,
|
...normalized,
|
||||||
|
timestamp: normalized.timestamp || prev.timestamp,
|
||||||
firstSavedAt: prev.firstSavedAt,
|
firstSavedAt: prev.firstSavedAt,
|
||||||
lastSeenAt: now,
|
lastSeenAt: now,
|
||||||
|
raw: { ...prev.raw, ...raw },
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
merged[key] = normalized;
|
merged[key] = normalized;
|
||||||
@@ -150,3 +156,54 @@ export function listArchivedNotifications(archive: ArchiveMap): ArchivedNotifica
|
|||||||
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
|
(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
import type { PluginAPI } from "../../core/types";
|
|
||||||
import type { ArchivedNotification, ArchiveMap } from "./archive";
|
|
||||||
import { listArchivedNotifications } from "./archive";
|
|
||||||
import { delay } from "@/seqta/utils/delay";
|
|
||||||
import ReactFiber from "@/seqta/utils/ReactFiber";
|
|
||||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
|
||||||
|
|
||||||
interface ArchiveUIStorage {
|
|
||||||
archivesByUser?: Record<string, ArchiveMap>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatArchiveDate(timestamp: string): string {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
if (Number.isNaN(date.getTime())) return timestamp;
|
|
||||||
return date.toLocaleString(undefined, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openArchivedMessage(item: ArchivedNotification) {
|
|
||||||
if (!item.messageID) return;
|
|
||||||
|
|
||||||
location.hash = `#?page=/messages`;
|
|
||||||
|
|
||||||
await waitForElm('[class*="Viewer__Viewer___"] > div', true, 40);
|
|
||||||
|
|
||||||
ReactFiber.find('[class*="Viewer__Viewer___"] > div').setState({
|
|
||||||
selected: new Set([item.messageID]),
|
|
||||||
});
|
|
||||||
|
|
||||||
await delay(10);
|
|
||||||
|
|
||||||
const row = document.querySelector('[class*="MessageList__selected___"]');
|
|
||||||
if (row) (row as HTMLElement).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function openArchivedAssessment(item: ArchivedNotification) {
|
|
||||||
if (!item.programmeID || !item.metaclassID) return;
|
|
||||||
const base = `#?page=/assessments/${item.programmeID}:${item.metaclassID}`;
|
|
||||||
location.hash = item.assessmentID ? `${base}&item=${item.assessmentID}` : base;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderArchiveItem(item: ArchivedNotification): HTMLElement {
|
|
||||||
const row = document.createElement("button");
|
|
||||||
row.type = "button";
|
|
||||||
row.className = "bsplus-notification-archive-item";
|
|
||||||
row.dataset.notificationId = String(item.notificationID);
|
|
||||||
|
|
||||||
const title = document.createElement("span");
|
|
||||||
title.className = "bsplus-notification-archive-title";
|
|
||||||
title.textContent = item.title;
|
|
||||||
|
|
||||||
const meta = document.createElement("span");
|
|
||||||
meta.className = "bsplus-notification-archive-meta";
|
|
||||||
const parts = [formatArchiveDate(item.timestamp)];
|
|
||||||
if (item.subtitle) parts.push(item.subtitle);
|
|
||||||
if (item.subjectCode) parts.push(item.subjectCode);
|
|
||||||
meta.textContent = parts.join(" · ");
|
|
||||||
|
|
||||||
const badge = document.createElement("span");
|
|
||||||
badge.className = "bsplus-notification-archive-badge";
|
|
||||||
badge.textContent = "Saved locally";
|
|
||||||
|
|
||||||
row.append(title, meta, badge);
|
|
||||||
|
|
||||||
row.addEventListener("click", () => {
|
|
||||||
if (item.type === "message") {
|
|
||||||
void openArchivedMessage(item);
|
|
||||||
} else if (item.type === "coneqtassessments") {
|
|
||||||
openArchivedAssessment(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mountArchiveUI(
|
|
||||||
api: PluginAPI<Record<string, never>, ArchiveUIStorage>,
|
|
||||||
getUserKey: () => Promise<string | null>,
|
|
||||||
) {
|
|
||||||
const mountOnList = (listEl: HTMLElement) => {
|
|
||||||
if (listEl.querySelector(".bsplus-notification-archive-bar")) return;
|
|
||||||
|
|
||||||
const bar = document.createElement("div");
|
|
||||||
bar.className = "bsplus-notification-archive-bar";
|
|
||||||
|
|
||||||
const toggle = document.createElement("button");
|
|
||||||
toggle.type = "button";
|
|
||||||
toggle.className = "bsplus-notification-archive-toggle";
|
|
||||||
|
|
||||||
const panel = document.createElement("div");
|
|
||||||
panel.className = "bsplus-notification-archive-panel";
|
|
||||||
panel.hidden = true;
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
const userKey = await getUserKey();
|
|
||||||
if (!userKey) {
|
|
||||||
toggle.textContent = "Saved notifications (sign in required)";
|
|
||||||
toggle.disabled = true;
|
|
||||||
panel.replaceChildren();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const archivesByUser = api.storage.archivesByUser ?? {};
|
|
||||||
const archive = archivesByUser[userKey] ?? {};
|
|
||||||
const items = listArchivedNotifications(archive);
|
|
||||||
const liveIds = new Set(
|
|
||||||
Array.from(listEl.querySelectorAll('[class*="notifications__item___"]'))
|
|
||||||
.map((el) => el.getAttribute("data-id"))
|
|
||||||
.filter(Boolean),
|
|
||||||
);
|
|
||||||
const removedCount = items.filter(
|
|
||||||
(item) => !liveIds.has(String(item.notificationID)),
|
|
||||||
).length;
|
|
||||||
|
|
||||||
toggle.disabled = false;
|
|
||||||
toggle.textContent =
|
|
||||||
items.length === 0
|
|
||||||
? "Saved notifications (none yet)"
|
|
||||||
: `Saved notifications (${items.length}${removedCount > 0 ? `, ${removedCount} no longer on SEQTA` : ""})`;
|
|
||||||
|
|
||||||
panel.replaceChildren();
|
|
||||||
if (items.length === 0) {
|
|
||||||
const empty = document.createElement("p");
|
|
||||||
empty.className = "bsplus-notification-archive-empty";
|
|
||||||
empty.textContent =
|
|
||||||
"Notifications you receive are saved here per account in extension storage.";
|
|
||||||
panel.append(empty);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
panel.append(renderArchiveItem(item));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
toggle.addEventListener("click", () => {
|
|
||||||
panel.hidden = !panel.hidden;
|
|
||||||
toggle.setAttribute("aria-expanded", panel.hidden ? "false" : "true");
|
|
||||||
if (!panel.hidden) void refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
bar.append(toggle, panel);
|
|
||||||
listEl.append(bar);
|
|
||||||
void refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
api.seqta.onMount('[class*="notifications__list___"]', (el) => {
|
|
||||||
mountOnList(el as HTMLElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
api.storage.onChange("archivesByUser", () => {
|
|
||||||
document
|
|
||||||
.querySelectorAll('[class*="notifications__list___"]')
|
|
||||||
.forEach((el) => {
|
|
||||||
const bar = el.querySelector(".bsplus-notification-archive-bar");
|
|
||||||
if (bar) bar.remove();
|
|
||||||
mountOnList(el as HTMLElement);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -8,15 +8,17 @@ import {
|
|||||||
mergeNotificationsIntoArchive,
|
mergeNotificationsIntoArchive,
|
||||||
resolveNotificationUserKey,
|
resolveNotificationUserKey,
|
||||||
} from "./archive";
|
} from "./archive";
|
||||||
import { mountArchiveUI } from "./archiveUI";
|
import {
|
||||||
import styles from "./styles.css?inline";
|
injectArchivedForUser,
|
||||||
|
mountArchivedNotificationInjection,
|
||||||
|
} from "./injectArchivedNotifications";
|
||||||
|
|
||||||
const notificationCollectorSettings = {
|
const notificationCollectorSettings = {
|
||||||
saveLocally: booleanSetting({
|
saveLocally: booleanSetting({
|
||||||
default: true,
|
default: true,
|
||||||
title: "Save notification history locally",
|
title: "Save notification history locally",
|
||||||
description:
|
description:
|
||||||
"Keeps a per-account copy in extension storage. SEQTA removes notifications after about a year; these stay until you clear extension data.",
|
"Saves notifications per account in extension storage and restores missing ones into the SEQTA list",
|
||||||
}),
|
}),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -35,9 +37,8 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
name: "Notification Collector",
|
name: "Notification Collector",
|
||||||
description:
|
description:
|
||||||
"Tracks notifications and saves a local per-account archive that outlasts SEQTA's server retention",
|
"Tracks notifications and saves a local per-account archive that outlasts SEQTA's server retention",
|
||||||
version: "1.1.0",
|
version: "1.2.0",
|
||||||
settings: notificationCollectorSettings,
|
settings: notificationCollectorSettings,
|
||||||
styles,
|
|
||||||
disableToggle: true,
|
disableToggle: true,
|
||||||
|
|
||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
@@ -80,6 +81,8 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
if (JSON.stringify(existing) !== JSON.stringify(merged)) {
|
if (JSON.stringify(existing) !== JSON.stringify(merged)) {
|
||||||
archivesByUser[userKey] = merged;
|
archivesByUser[userKey] = merged;
|
||||||
api.storage.archivesByUser = archivesByUser;
|
api.storage.archivesByUser = archivesByUser;
|
||||||
|
} else if (document.querySelector('[class*="notifications__list___"]')) {
|
||||||
|
await injectArchivedForUser(merged);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[BetterSEQTA+] Notification archive sync failed:", error);
|
console.warn("[BetterSEQTA+] Notification archive sync failed:", error);
|
||||||
@@ -182,7 +185,10 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mountArchiveUI(api, resolveNotificationUserKey);
|
const teardownInjection = mountArchivedNotificationInjection(
|
||||||
|
api,
|
||||||
|
resolveNotificationUserKey,
|
||||||
|
);
|
||||||
|
|
||||||
api.seqta.onMount("[class*='notifications__bubble___']", () => {
|
api.seqta.onMount("[class*='notifications__bubble___']", () => {
|
||||||
startPolling();
|
startPolling();
|
||||||
@@ -199,6 +205,7 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
teardownInjection();
|
||||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
pageChangeUnregister.unregister();
|
pageChangeUnregister.unregister();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import type { PluginAPI } from "../../core/types";
|
||||||
|
import ReactFiber from "@/seqta/utils/ReactFiber";
|
||||||
|
import { delay } from "@/seqta/utils/delay";
|
||||||
|
import {
|
||||||
|
archivedToApiNotification,
|
||||||
|
listArchivedNotifications,
|
||||||
|
type ArchiveMap,
|
||||||
|
type ArchivesByUser,
|
||||||
|
} from "./archive";
|
||||||
|
|
||||||
|
const LIST_SELECTOR = '[class*="notifications__list___"]';
|
||||||
|
const ITEMS_SELECTOR = '[class*="notifications__items___"]';
|
||||||
|
|
||||||
|
function notificationTimestamp(item: Record<string, unknown>): number {
|
||||||
|
const ms = new Date(String(item.timestamp ?? 0)).getTime();
|
||||||
|
return Number.isNaN(ms) ? 0 : ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeLiveWithArchived(
|
||||||
|
liveItems: Record<string, unknown>[],
|
||||||
|
archive: ArchiveMap,
|
||||||
|
): Record<string, unknown>[] | null {
|
||||||
|
const liveIds = new Set(liveItems.map((item) => Number(item.notificationID)));
|
||||||
|
const missing = listArchivedNotifications(archive)
|
||||||
|
.filter((item) => !liveIds.has(item.notificationID))
|
||||||
|
.map((item) => archivedToApiNotification(item));
|
||||||
|
|
||||||
|
if (missing.length === 0) return null;
|
||||||
|
|
||||||
|
return [...liveItems, ...missing].sort(
|
||||||
|
(a, b) => notificationTimestamp(b) - notificationTimestamp(a),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const state = await ReactFiber.find(LIST_SELECTOR).getState();
|
||||||
|
if (!state || !Array.isArray(state.items)) return false;
|
||||||
|
|
||||||
|
const liveItems = state.items as Record<string, unknown>[];
|
||||||
|
const merged = mergeLiveWithArchived(liveItems, archive);
|
||||||
|
if (!merged) return true;
|
||||||
|
if (sameItemOrder(liveItems, merged)) 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) return;
|
||||||
|
await delay(120);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mountArchivedNotificationInjection(
|
||||||
|
api: PluginAPI<Record<string, never>, { archivesByUser?: ArchivesByUser }>,
|
||||||
|
getUserKey: () => Promise<string | null>,
|
||||||
|
) {
|
||||||
|
let observer: MutationObserver | null = null;
|
||||||
|
let injectScheduled = false;
|
||||||
|
|
||||||
|
const scheduleInject = () => {
|
||||||
|
if (injectScheduled) return;
|
||||||
|
injectScheduled = true;
|
||||||
|
window.setTimeout(async () => {
|
||||||
|
injectScheduled = false;
|
||||||
|
const userKey = await getUserKey();
|
||||||
|
if (!userKey) return;
|
||||||
|
const archive = api.storage.archivesByUser?.[userKey] ?? {};
|
||||||
|
if (Object.keys(archive).length === 0) return;
|
||||||
|
await injectWithRetries(archive);
|
||||||
|
}, 60);
|
||||||
|
};
|
||||||
|
|
||||||
|
const watchItemsContainer = () => {
|
||||||
|
const itemsEl = document.querySelector(ITEMS_SELECTOR);
|
||||||
|
if (!itemsEl) return;
|
||||||
|
if (observer) observer.disconnect();
|
||||||
|
observer = new MutationObserver(() => scheduleInject());
|
||||||
|
observer.observe(itemsEl, { childList: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
api.seqta.onMount(LIST_SELECTOR, () => {
|
||||||
|
scheduleInject();
|
||||||
|
watchItemsContainer();
|
||||||
|
});
|
||||||
|
|
||||||
|
api.seqta.onMount(ITEMS_SELECTOR, () => {
|
||||||
|
watchItemsContainer();
|
||||||
|
scheduleInject();
|
||||||
|
});
|
||||||
|
|
||||||
|
api.storage.onChange("archivesByUser", () => scheduleInject());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observer) observer.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function injectArchivedForUser(
|
||||||
|
archive: ArchiveMap,
|
||||||
|
): Promise<void> {
|
||||||
|
await injectWithRetries(archive);
|
||||||
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
.bsplus-notification-archive-bar {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
border-top: 1px solid color-mix(in srgb, var(--text-primary) 12%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-toggle {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid color-mix(in srgb, var(--text-primary) 14%, transparent);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--theme-primary, var(--background-primary, #232323));
|
|
||||||
color: var(--text-primary);
|
|
||||||
padding: 0.55rem 0.75rem;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 150ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-toggle:hover:not(:disabled) {
|
|
||||||
background: var(--theme-secondary, var(--background-secondary, #1a1a1a));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-toggle:disabled {
|
|
||||||
opacity: 0.65;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-panel {
|
|
||||||
margin-top: 0.45rem;
|
|
||||||
max-height: 14rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-panel[hidden] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.2rem;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
border-left: 3px solid var(--better-main, #3b82f6);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--theme-primary, var(--background-primary, #232323));
|
|
||||||
color: var(--text-primary);
|
|
||||||
padding: 0.5rem 0.65rem;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 150ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-item:hover {
|
|
||||||
background: var(--theme-secondary, var(--background-secondary, #1a1a1a));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-title {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-meta {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
opacity: 0.78;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-badge {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-notification-archive-empty {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user