import { Icon } from "$/components/shared/Icon"; import { useAppSession } from "$/contexts/app-session"; import { FeedController } from "$/lib/api/feeds"; import { findRootPost, isBlockedNode, isNotFoundNode, isThreadViewPost, patchThreadNode } from "$/lib/feeds"; import { useNavigationHistory } from "$/lib/navigation-history"; import type { PostView, ThreadNode } from "$/lib/types"; import { createEffect, createMemo, For, Match, onCleanup, Show, splitProps, Switch } from "solid-js"; import { createStore } from "solid-js/store"; import { Motion, Presence } from "solid-motionone"; import { PostCard } from "../feeds/PostCard"; import { HistoryControls } from "../shared/HistoryControls"; import { usePostInteractions } from "./hooks/usePostInteractions"; import { usePostNavigation } from "./hooks/usePostNavigation"; import { useThreadOverlayNavigation } from "./hooks/useThreadOverlayNavigation"; type ThreadDrawerState = { error: string | null; loading: boolean; thread: ThreadNode | null; uri: string | null }; function createThreadDrawerState(): ThreadDrawerState { return { error: null, loading: false, thread: null, uri: null }; } function findParentUri(node: ThreadNode | null, targetUri: string | null): string | null { if (!node || !targetUri) { return null; } const visited = new Set(); function walk(current: ThreadNode): string | null { if (visited.has(current)) { return null; } visited.add(current); if (isThreadViewPost(current)) { if (current.post.uri === targetUri && current.parent && isThreadViewPost(current.parent)) { return current.parent.post.uri; } if (current.parent) { const parentMatch = walk(current.parent); if (parentMatch) { return parentMatch; } } for (const reply of current.replies ?? []) { const replyMatch = walk(reply); if (replyMatch) { return replyMatch; } } } return null; } return walk(node); } function createEscapeKeyHandler(onClose: () => void) { return (event: KeyboardEvent) => { if (event.key !== "Escape") { return; } event.preventDefault(); onClose(); }; } type ThreadDrawerBodyProps = { activeUri: string | null; bookmarkPendingByUri: Record; error: string | null; likePendingByUri: Record; loading: boolean; onBookmark: (post: PostView) => void; onLike: (post: PostView) => void; onOpenEngagement: (uri: string, tab: "likes" | "reposts" | "quotes") => void; onOpenThread: (uri: string) => void; onRepost: (post: PostView) => void; repostPendingByUri: Record; rootPost: PostView | null; thread: ThreadNode | null; }; function ThreadDrawerBody(props: ThreadDrawerBodyProps) { return (
{(message) => (
{message()}
)}
{(root) => (
)}
); } type ThreadDrawerHeaderProps = { activeUri: string | null; canGoBack: boolean; canGoForward: boolean; onClose: () => void; onGoBack: () => void; onGoForward: () => void; onMaximize: (uri: string) => void; parentThreadHref: string | null; }; function ThreadDrawerHeader(props: ThreadDrawerHeaderProps) { const [local, historyControls] = splitProps(props, ["parentThreadHref", "activeUri", "onClose", "onMaximize"]); return (

Thread!

{(href) => ( Parent post )}
{(uri) => ( )}
); } function ThreadDrawerLoading(props: { loading: boolean }) { return (
); } function ThreadNodeView( props: { activeUri: string | null; bookmarkPendingByUri: Record; likePendingByUri: Record; node: ThreadNode; onBookmark: (post: PostView) => void; onLike: (post: PostView) => void; onOpenEngagement: (uri: string, tab: "likes" | "reposts" | "quotes") => void; onOpenThread: (uri: string) => void; onRepost: (post: PostView) => void; repostPendingByUri: Record; rootPost: PostView; }, ) { const node = createMemo(() => (isThreadViewPost(props.node) ? props.node : null)); return ( {(threadNode) => (
{(parent) => (
)}
props.onBookmark(threadNode().post)} onLike={() => props.onLike(threadNode().post)} onOpenEngagement={(tab) => props.onOpenEngagement(threadNode().post.uri, tab)} onOpenThread={() => props.onOpenThread(threadNode().post.uri)} onRepost={() => props.onRepost(threadNode().post)} post={threadNode().post} repostPending={!!props.repostPendingByUri[threadNode().post.uri]} />
{(reply) => ( )}
)}
); } function ThreadStateCard(props: { label: string; meta: string }) { return (

{props.label}

{props.meta}

); } function ThreadSkeletonCard() { return (
); } export function ThreadDrawer() { const session = useAppSession(); const postNavigation = usePostNavigation(); const threadOverlay = useThreadOverlayNavigation(); const history = useNavigationHistory(); const [state, setState] = createStore(createThreadDrawerState()); const activeUri = createMemo(() => (threadOverlay.drawerEnabled() ? threadOverlay.threadUri() : null)); const rootPost = createMemo(() => findRootPost(state.thread)); const parentThreadUri = createMemo(() => findParentUri(state.thread, activeUri())); const parentThreadHref = createMemo(() => parentThreadUri() ? threadOverlay.buildThreadHref(parentThreadUri()) : null ); const interactions = usePostInteractions({ onError: session.reportError, patchPost(uri, updater) { const current = state.thread; if (!current) { return; } setState("thread", patchThreadNode(current, uri, updater)); }, }); createEffect(() => { const uri = activeUri(); if (!uri) { if (state.uri || state.thread || state.error || state.loading) { setState(createThreadDrawerState()); } return; } if (state.uri === uri && (state.loading || state.thread || state.error)) { return; } void loadThread(uri); }); createEffect(() => { if (!activeUri()) { return; } const handleKeyDown = createEscapeKeyHandler(() => { void threadOverlay.closeThread(); }); globalThis.addEventListener("keydown", handleKeyDown); onCleanup(() => globalThis.removeEventListener("keydown", handleKeyDown)); }); async function loadThread(uri: string) { setState({ error: null, loading: true, thread: null, uri }); try { const payload = await FeedController.getPostThread(uri); if (activeUri() === uri) { setState({ error: null, loading: false, thread: payload.thread, uri }); } } catch (error) { if (activeUri() === uri) { setState({ error: String(error), loading: false, thread: null, uri }); } session.reportError(`Failed to open thread: ${String(error)}`); } } return (
void threadOverlay.closeThread()} /> void postNavigation.openPostScreen(uri)} parentThreadHref={parentThreadHref()} onClose={() => void threadOverlay.closeThread()} /> void interactions.toggleBookmark(post)} onLike={(post) => void interactions.toggleLike(post)} onOpenEngagement={(uri, tab) => void postNavigation.openPostEngagement(uri, tab)} onOpenThread={(uri) => void threadOverlay.openThread(uri)} onRepost={(post) => void interactions.toggleRepost(post)} repostPendingByUri={interactions.repostPendingByUri()} rootPost={rootPost()} thread={state.thread} />
); }