1import 'fixi-js';2import '@bigskysoftware/paxi-js';3import '@bigskysoftware/ssexi-js';45import { element, escapeHtml, setPageTitle } from './shared';6import { clearTheme, updateLogoContrast } from './theme';78interface FixiConfig {9 action: string;10 response: Response;11 ssePauseOnHidden?: boolean;12 sseReconnect?: boolean;13 sseSwap?: (target: Element, html: string) => void;14 text: string;15 target: Element;16}1718interface FixiEventDetail {19 cfg: FixiConfig;20}2122interface SsexiEventDetail extends FixiEventDetail {23 message: {24 data: string;25 event: string;26 id: string;27 };28}2930declare global {31 interface Window {32 morph(target: Element, html: string): void;33 }34}3536const main = element('main');37let folderState = new Map<string, boolean>();3839function profileRequest(config: FixiConfig): boolean {40 return config.action.startsWith('/ui/profile/');41}4243function renderMissingProfile(handle: string, message = 'does not exist') {44 clearTheme();45 main.removeAttribute('data-profile-handle');46 main.removeAttribute('data-profile-scheme');47 main.removeAttribute('aria-busy');48 main.innerHTML = `49 <article>50 <hgroup>51 <h1>No such profile.</h1>52 <p>@${escapeHtml(handle)} ${escapeHtml(message)}</p>53 </hgroup>54 </article>`;55}5657document.addEventListener('fx:config', (event) => {58 const config = (event as CustomEvent<FixiEventDetail>).detail.cfg;59 if (!profileRequest(config)) return;60 config.sseReconnect = true;61 config.ssePauseOnHidden = true;62 config.sseSwap = (target, html) => window.morph(target, html);63});6465document.addEventListener('fx:sse:message', (event) => {66 const detail = (event as CustomEvent<SsexiEventDetail>).detail;67 if (!profileRequest(detail.cfg)) return;68 folderState = new Map(69 [...main.querySelectorAll<HTMLDetailsElement>('details[data-folder]')]70 .map((folder) => [folder.dataset.folder ?? '', folder.open]),71 );72});7374document.addEventListener('fx:sse:swapped', (event) => {75 const detail = (event as CustomEvent<SsexiEventDetail>).detail;76 if (!profileRequest(detail.cfg) || event.target !== main) return;77 const handle = main.dataset.profileHandle;78 const scheme = main.dataset.profileScheme;79 if (!handle) return;80 document.documentElement.dataset.profile = handle;81 if (scheme === 'light' || scheme === 'dark') {82 document.documentElement.dataset.theme = scheme;83 } else {84 delete document.documentElement.dataset.theme;85 }86 for (const folder of main.querySelectorAll<HTMLDetailsElement>('details[data-folder]')) {87 const open = folderState.get(folder.dataset.folder ?? '');88 if (open !== undefined) folder.open = open;89 }90 folderState.clear();91 setPageTitle(`@${handle}`);92 requestAnimationFrame(updateLogoContrast);93});9495document.addEventListener('fx:after', (event) => {96 const detail = (event as CustomEvent<FixiEventDetail>).detail;97 if (!profileRequest(detail.cfg) || detail.cfg.response.ok) return;98 event.preventDefault();99 renderMissingProfile(decodeURIComponent(location.pathname.slice(2)));100});101102document.addEventListener('fx:sse:unavailable', () => {103 renderMissingProfile(decodeURIComponent(location.pathname.slice(2)));104});105106export function showProfile(handle: string) {107 setPageTitle(`@${handle}`);108 clearTheme();109 main.setAttribute('aria-busy', 'true');110 main.innerHTML = '<article><p>Loading profile.</p></article>';111 main.setAttribute('fx-action', `/ui/profile/${encodeURIComponent(handle)}/stream`);112 main.setAttribute('fx-method', 'GET');113 main.setAttribute('fx-swap', 'morph');114 main.setAttribute('fx-trigger', 'profile:load');115 main.dispatchEvent(new CustomEvent('fx:process', { bubbles: true, composed: true }));116 main.dispatchEvent(new Event('profile:load'));117}