import clsx from 'clsx'; import { Bell, LogOut, RotateCcw, Save, Search, Settings } from 'lucide-solid'; import { A, useLocation, useNavigate } from '@solidjs/router'; import { createQuery, useQueryClient } from '@tanstack/solid-query'; import { For, Show, createEffect, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js'; import { Avatar, buttonStyles, inputStyles } from './components/common'; import { clearApiCaches, resolveActor } from './lib/api/identity'; import { useAuth } from './lib/auth'; import { useLiveEvents, type LiveEvent } from './lib/live-events'; import { formatRelativeTime } from './lib/repo-utils'; import { useAppSettings } from './lib/settings'; const TOPBAR_MENU_SELECTOR = 'details[data-topbar-menu]'; const closeOpenTopbarMenus = (root: HTMLElement, target: Node) => { root.querySelectorAll(TOPBAR_MENU_SELECTOR).forEach((menu) => { if (menu.open && !menu.contains(target)) { menu.open = false; } }); }; const eventLabel = (event: LiveEvent) => { if (event.source === 'sh.tangled.repo.issue:repo') { return `new issue on ${event.subject}`; } if (event.source === 'sh.tangled.repo.pull:target.repo') { return `new pull on ${event.subject}`; } return `${event.source} -> ${event.subject}`; }; const AppSettingsMenu: Component = () => { const settings = useAppSettings(); const queryClient = useQueryClient(); const [draftAppviewUrl, setDraftAppviewUrl] = createSignal(settings.appviewUrl()); const [draftRouteKnotRequestsThroughAppview, setDraftRouteKnotRequestsThroughAppview] = createSignal( settings.routeKnotRequestsThroughAppview(), ); const [error, setError] = createSignal(null); const [saved, setSaved] = createSignal(false); createEffect(() => { setDraftAppviewUrl(settings.appviewUrl()); setDraftRouteKnotRequestsThroughAppview(settings.routeKnotRequestsThroughAppview()); }); const reloadAppviewBackedQueries = () => { clearApiCaches(); void queryClient.invalidateQueries(); }; const onSave = (event: SubmitEvent) => { event.preventDefault(); try { settings.setAppviewUrl(draftAppviewUrl()); settings.setRouteKnotRequestsThroughAppview(draftRouteKnotRequestsThroughAppview()); setError(null); setSaved(true); reloadAppviewBackedQueries(); } catch (cause) { setSaved(false); setError(cause instanceof Error ? cause.message : 'Invalid appview URL'); } }; const onReset = () => { settings.resetAppviewSettings(); setDraftAppviewUrl(settings.defaultAppviewUrl); setDraftRouteKnotRequestsThroughAppview(false); setError(null); setSaved(true); reloadAppviewBackedQueries(); }; return (
{ setDraftAppviewUrl(event.currentTarget.value); setSaved(false); setError(null); }} placeholder={settings.defaultAppviewUrl} class={inputStyles()} />

{error()}

saved

); }; const LoginMenu: Component = () => { const auth = useAuth(); const [identifier, setIdentifier] = createSignal(''); const onSignIn = async (event: SubmitEvent) => { event.preventDefault(); await auth.signIn(identifier()); }; return (
login
setIdentifier(event.currentTarget.value)} placeholder="handle or did" class={inputStyles()} />

{auth.error()}

); }; const NotificationsMenu: Component = () => { const live = useLiveEvents(); return (
live.markRead()} >
0}> {Math.min(live.unread(), 99)}
live activity
0} fallback={
No live events yet.
} > {(event) => (
{eventLabel(event)}
{formatRelativeTime(event.receivedAt)}
)}
); }; const UserProfileMenu: Component = () => { const auth = useAuth(); const viewerQuery = createQuery(() => ({ queryKey: ['viewer', auth.currentDid()], enabled: Boolean(auth.currentDid()), queryFn: async () => resolveActor(auth.currentDid()!), })); return (
{viewerQuery.data?.handle ?? auth.currentDid()}
{auth.currentDid()}
); }; const TopbarSearch: Component = () => { const navigate = useNavigate(); const location = useLocation(); const [query, setQuery] = createSignal(''); let inputRef: HTMLInputElement | undefined; onMount(() => { const handler = (e: KeyboardEvent) => { if (e.ctrlKey || e.metaKey) return; if (e.key === "/") { e.preventDefault(); inputRef?.focus(); } else if (e.key === "Escape") { setQuery(''); inputRef?.blur(); } } window.addEventListener("keydown", handler) onCleanup(() => window.removeEventListener("keydown", handler)) }); createEffect(() => { if (location.pathname !== '/search') { setQuery(''); return; } const params = new URLSearchParams(location.search); setQuery(params.get('q') ?? ''); }); const onSubmit = (event: SubmitEvent) => { event.preventDefault(); const trimmed = query().trim(); navigate(trimmed ? `/search?q=${encodeURIComponent(trimmed)}` : '/search'); }; return ( ); }; const Topbar: Component = () => { const auth = useAuth(); let topbar: HTMLElement | undefined; onMount(() => { const closeMenusForOutsideClick = (event: PointerEvent) => { const target = event.target; if (!topbar || !(target instanceof Node)) { return; } closeOpenTopbarMenus(topbar, target); }; document.addEventListener('pointerdown', closeMenusForOutsideClick); onCleanup(() => { document.removeEventListener('pointerdown', closeMenusForOutsideClick); }); }); return ( ); }; export const RootShell: Component<{ children?: JSX.Element }> = (props) => (
{props.children}
);