diff --git a/src/seqta/utils/Loaders/LoadEngageHomePage.ts b/src/seqta/utils/Loaders/LoadEngageHomePage.ts index e4c57b5c..04dd0d90 100644 --- a/src/seqta/utils/Loaders/LoadEngageHomePage.ts +++ b/src/seqta/utils/Loaders/LoadEngageHomePage.ts @@ -18,6 +18,10 @@ import { toISODate, weekRangeContaining, } from "@/seqta/utils/Loaders/engageParentTimetable"; +import { + noticeMatchesLabelFilter, + resolveNoticeFilterTokens, +} from "@/seqta/utils/notices/noticeLabelFilters"; export function updateEngageHomeMenuActive(isHome: boolean): void { const home = document.getElementById("homebutton"); @@ -267,30 +271,17 @@ function processEngageNotices(response: any, labelArray: string[]): void { const noticeContainer = document.getElementById(ENGAGE_NOTICE_CONTAINER_ID); if (!noticeContainer) return; + noticeContainer.classList.remove("loading"); noticeContainer.innerHTML = ""; const notices = response?.payload; if (!Array.isArray(notices)) { - const emptyState = document.createElement("div"); - emptyState.classList.add("day-empty"); - const img = document.createElement("img"); - img.src = browser.runtime.getURL(LogoLight); - const text = document.createElement("p"); - text.innerText = "No notices for today."; - emptyState.append(img, text); - noticeContainer.append(emptyState); + appendEngageNoticeEmptyState(noticeContainer, "No notices for today."); return; } if (!notices.length) { - const emptyState = document.createElement("div"); - emptyState.classList.add("day-empty"); - const img = document.createElement("img"); - img.src = browser.runtime.getURL(LogoLight); - const text = document.createElement("p"); - text.innerText = "No notices for today."; - emptyState.append(img, text); - noticeContainer.append(emptyState); + appendEngageNoticeEmptyState(noticeContainer, "No notices for today."); return; } @@ -298,9 +289,7 @@ function processEngageNotices(response: any, labelArray: string[]): void { notices.forEach((notice: any) => { const shouldInclude = - settingsState.mockNotices || - labelArray.length === 0 || - labelArray.includes(JSON.stringify(notice.label)); + settingsState.mockNotices || noticeMatchesLabelFilter(notice, labelArray); if (shouldInclude) { const colour = processEngageNoticeColor(notice.colour); @@ -309,9 +298,25 @@ function processEngageNotices(response: any, labelArray: string[]): void { } }); + if (fragment.childNodes.length === 0) { + appendEngageNoticeEmptyState(noticeContainer, "No notices for today."); + return; + } + noticeContainer.appendChild(fragment); } +function appendEngageNoticeEmptyState(container: HTMLElement, message: string) { + const emptyState = document.createElement("div"); + emptyState.classList.add("day-empty"); + const img = document.createElement("img"); + img.src = browser.runtime.getURL(LogoLight); + const text = document.createElement("p"); + text.innerText = message; + emptyState.append(img, text); + container.append(emptyState); +} + function createEngageNoticeElement( notice: any, colour: string | undefined, @@ -613,6 +618,12 @@ async function fetchEngageNoticesFromApi( date: string, labelTokens: string[], ): Promise { + const noticeContainer = document.getElementById(ENGAGE_NOTICE_CONTAINER_ID); + if (noticeContainer) { + noticeContainer.classList.add("loading"); + noticeContainer.innerHTML = ""; + } + try { const data = settingsState.mockNotices ? getMockNotices() @@ -662,7 +673,7 @@ async function initEngageNoticesUi(todayFormatted: string): Promise { const noticeContainer = document.getElementById(ENGAGE_NOTICE_CONTAINER_ID); if (!noticeContainer) return; - let labelFilterValues: string[] = []; + let prefsPayload: unknown = []; try { const prefsRes = await fetch(`${location.origin}/seqta/parent/load/prefs?`, { method: "POST", @@ -671,21 +682,15 @@ async function initEngageNoticesUi(todayFormatted: string): Promise { body: JSON.stringify({ asArray: true, request: "userPrefs" }), }); const prefs = await prefsRes.json(); - const payload = prefs?.payload; - if (Array.isArray(payload)) { - labelFilterValues = payload - .filter((item: { name?: string }) => item.name === "notices.filters") - .map((item: { value?: string }) => item.value) - .filter((v): v is string => typeof v === "string"); - } + prefsPayload = prefs?.payload ?? []; } catch { - labelFilterValues = []; + prefsPayload = []; } - const labelTokens = - labelFilterValues.length > 0 - ? String(labelFilterValues[0]).split(" ").filter(Boolean) - : []; + const labelTokens = await resolveNoticeFilterTokens( + prefsPayload, + `${location.origin}/seqta/parent/load/notices`, + ); const dateControl = document.getElementById(ENGAGE_NOTICES_DATE_ID); if (dateControl) { @@ -696,8 +701,6 @@ async function initEngageNoticesUi(todayFormatted: string): Promise { const cleanup = bindEngageNoticesDateInput(labelTokens, todayFormatted); engageMergeNoticeCleanup(cleanup); - - noticeContainer.classList.remove("loading"); } function engageMergeNoticeCleanup(noticeCleanup: () => void): void { diff --git a/src/seqta/utils/Loaders/LoadHomePage.ts b/src/seqta/utils/Loaders/LoadHomePage.ts index f922a9fe..6e66b2ce 100644 --- a/src/seqta/utils/Loaders/LoadHomePage.ts +++ b/src/seqta/utils/Loaders/LoadHomePage.ts @@ -20,6 +20,10 @@ import { filterAssessmentsForActiveSubjects, subjectsWithUpcomingAssessments, } from "@/plugins/built-in/assessmentsOverview/utils"; +import { + noticeMatchesLabelFilter, + resolveNoticeFilterTokens, +} from "@/seqta/utils/notices/noticeLabelFilters"; let LessonInterval: any; let currentSelectedDate = new Date(); @@ -131,33 +135,20 @@ export async function loadHomePage() { upcomingItems.classList.remove("loading"); } - const labelArray = prefs.payload - .filter((item: any) => item.name === "notices.filters") - .map((item: any) => item.value); + const labelTokens = await resolveNoticeFilterTokens( + prefs.payload, + `${location.origin}/seqta/student/load/notices?`, + ); const noticeContainer = document.getElementById("notice-container"); if (noticeContainer) { - if (labelArray.length > 0) { - const dateControl = document.querySelector( - 'input[type="date"]', - ) as HTMLInputElement; - if (dateControl) { - dateControl.value = TodayFormatted; - setupNotices(labelArray[0].split(" "), TodayFormatted); - } - noticeContainer.classList.remove("loading"); - } else { - noticeContainer.classList.remove("loading"); - noticeContainer.innerHTML = ""; - const emptyState = document.createElement("div"); - emptyState.classList.add("day-empty"); - const img = document.createElement("img"); - img.src = browser.runtime.getURL(LogoLight); - const text = document.createElement("p"); - text.innerText = "No notices available."; - emptyState.append(img, text); - noticeContainer.append(emptyState); + const dateControl = document.querySelector( + 'input[type="date"]', + ) as HTMLInputElement; + if (dateControl) { + dateControl.value = TodayFormatted; } + setupNotices(labelTokens, TodayFormatted); } return cleanup; @@ -248,6 +239,12 @@ function setupNotices(labelArray: string[], date: string) { ) as HTMLInputElement; const fetchNotices = async (date: string) => { + const container = document.getElementById("notice-container"); + if (container) { + container.classList.add("loading"); + container.innerHTML = ""; + } + try { const data = settingsState.mockNotices ? getMockNotices() @@ -255,13 +252,13 @@ function setupNotices(labelArray: string[], date: string) { await fetch(`${location.origin}/seqta/student/load/notices?`, { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, + credentials: "include", body: JSON.stringify({ date }), }) ).json(); processNotices(data, labelArray); } catch { - // Notices failed to load; processNotices will show empty state if container exists processNotices({ payload: [] }, labelArray); } }; @@ -294,30 +291,17 @@ function processNotices(response: any, labelArray: string[]) { const NoticeContainer = document.getElementById("notice-container"); if (!NoticeContainer) return; + NoticeContainer.classList.remove("loading"); NoticeContainer.innerHTML = ""; const notices = response?.payload; if (!Array.isArray(notices)) { - const emptyState = document.createElement("div"); - emptyState.classList.add("day-empty"); - const img = document.createElement("img"); - img.src = browser.runtime.getURL(LogoLight); - const text = document.createElement("p"); - text.innerText = "No notices for today."; - emptyState.append(img, text); - NoticeContainer.append(emptyState); + appendNoticeEmptyState(NoticeContainer, "No notices for today."); return; } if (!notices.length) { - const emptyState = document.createElement("div"); - emptyState.classList.add("day-empty"); - const img = document.createElement("img"); - img.src = browser.runtime.getURL(LogoLight); - const text = document.createElement("p"); - text.innerText = "No notices for today."; - emptyState.append(img, text); - NoticeContainer.append(emptyState); + appendNoticeEmptyState(NoticeContainer, "No notices for today."); return; } @@ -325,8 +309,7 @@ function processNotices(response: any, labelArray: string[]) { notices.forEach((notice: any) => { const shouldInclude = - settingsState.mockNotices || - labelArray.includes(JSON.stringify(notice.label)); + settingsState.mockNotices || noticeMatchesLabelFilter(notice, labelArray); if (shouldInclude) { const colour = processNoticeColor(notice.colour); @@ -335,9 +318,25 @@ function processNotices(response: any, labelArray: string[]) { } }); + if (fragment.childNodes.length === 0) { + appendNoticeEmptyState(NoticeContainer, "No notices for today."); + return; + } + NoticeContainer.appendChild(fragment); } +function appendNoticeEmptyState(container: HTMLElement, message: string) { + const emptyState = document.createElement("div"); + emptyState.classList.add("day-empty"); + const img = document.createElement("img"); + img.src = browser.runtime.getURL(LogoLight); + const text = document.createElement("p"); + text.innerText = message; + emptyState.append(img, text); + container.append(emptyState); +} + function processNoticeColor(colour: string): string | undefined { if (typeof colour === "string") { const rgb = GetThresholdOfColor(colour); diff --git a/src/seqta/utils/notices/noticeLabelFilters.ts b/src/seqta/utils/notices/noticeLabelFilters.ts new file mode 100644 index 00000000..26818293 --- /dev/null +++ b/src/seqta/utils/notices/noticeLabelFilters.ts @@ -0,0 +1,73 @@ +type PrefEntry = { name?: string; value?: unknown }; + +/** Parse `notices.filters` pref (space-separated label IDs). */ +export function parseNoticesFilterPref(prefsPayload: unknown): string[] { + if (!Array.isArray(prefsPayload)) return []; + const values = (prefsPayload as PrefEntry[]) + .filter((item) => item?.name === "notices.filters") + .map((item) => item?.value) + .filter((v): v is string => typeof v === "string"); + if (values.length === 0) return []; + return String(values[0]).split(" ").filter(Boolean); +} + +/** Label IDs from `load/notices` with `{ mode: "labels" }`. */ +export async function fetchNoticeLabelIds(noticesUrl: string): Promise { + try { + const res = await fetch(noticesUrl, { + method: "POST", + headers: { "Content-Type": "application/json; charset=utf-8" }, + credentials: "include", + body: JSON.stringify({ mode: "labels" }), + }); + if (!res.ok) return []; + const json = (await res.json()) as { payload?: Array<{ id?: number }> }; + const payload = json?.payload; + if (!Array.isArray(payload)) return []; + return payload + .map((entry) => entry?.id) + .filter((id): id is number => typeof id === "number" && !Number.isNaN(id)) + .map(String); + } catch { + return []; + } +} + +/** Pref filters when set; otherwise all label IDs from the labels API (native SEQTA home). */ +export async function resolveNoticeFilterTokens( + prefsPayload: unknown, + noticesUrl: string, +): Promise { + const fromPref = parseNoticesFilterPref(prefsPayload); + if (fromPref.length > 0) return fromPref; + return await fetchNoticeLabelIds(noticesUrl); +} + +export function normalizeNoticeLabelId(label: unknown): string | null { + if (typeof label === "number" && !Number.isNaN(label)) { + return String(label); + } + if (typeof label === "string" && label.trim()) { + return label.trim(); + } + if (label && typeof label === "object") { + const obj = label as Record; + if (typeof obj.id === "number" && !Number.isNaN(obj.id)) { + return String(obj.id); + } + if (typeof obj.id === "string" && obj.id.trim()) { + return obj.id.trim(); + } + } + return null; +} + +export function noticeMatchesLabelFilter( + notice: { label?: unknown }, + filterTokens: string[], +): boolean { + if (filterTokens.length === 0) return true; + const id = normalizeNoticeLabelId(notice?.label); + if (id !== null && filterTokens.includes(id)) return true; + return filterTokens.includes(JSON.stringify(notice?.label)); +}