import { PostCard } from "$/components/feeds/PostCard"; import { Icon } from "$/components/shared/Icon"; import { useAppSession } from "$/contexts/app-session"; import { FeedController } from "$/lib/api/feeds"; import { patchThreadNode } from "$/lib/feeds"; import { isBlockedNode, isNotFoundNode, isThreadViewPost } from "$/lib/feeds/type-guards"; import type { PostView, ThreadNode, ThreadViewPost } from "$/lib/types"; import { createEffect, createMemo, For, Match, Show, Switch } from "solid-js"; import { createStore } from "solid-js/store"; import { usePostInteractions } from "./hooks/usePostInteractions"; import { usePostNavigation } from "./hooks/usePostNavigation"; type PostPanelState = { error: string | null; loading: boolean; thread: ThreadNode | null; uri: string | null }; function createPostPanelState(): PostPanelState { return { error: null, loading: false, thread: null, uri: null }; } function findThreadPost(node: ThreadNode | null | undefined, uri: string): ThreadViewPost | null { if (!node || !isThreadViewPost(node)) { return null; } if (node.post.uri === uri) { return node; } const parentMatch = findThreadPost(node.parent, uri); if (parentMatch) { return parentMatch; } for (const reply of node.replies ?? []) { const replyMatch = findThreadPost(reply, uri); if (replyMatch) { return replyMatch; } } return null; } function collectParentChain(node: ThreadViewPost | null): ThreadViewPost[] { if (!node) { return []; } const chain: ThreadViewPost[] = []; let current: ThreadNode | null | undefined = node.parent; while (current && isThreadViewPost(current)) { chain.unshift(current); current = current.parent; } return chain; } export function PostPanel(props: { uri: string | null }) { const session = useAppSession(); const postNavigation = usePostNavigation(); const [state, setState] = createStore(createPostPanelState()); let requestId = 0; const interactions = usePostInteractions({ onError: session.reportError, patchPost(uri, updater) { const current = state.thread; if (!current) { return; } setState("thread", patchThreadNode(current, uri, updater)); }, }); const focusedNode = createMemo(() => { const uri = props.uri; const thread = state.thread; if (!uri || !thread) { return null; } return findThreadPost(thread, uri); }); const parentChain = createMemo(() => collectParentChain(focusedNode())); const parentPostUri = createMemo(() => { const focused = focusedNode(); if (!focused || !focused.parent || !isThreadViewPost(focused.parent)) { return null; } return focused.parent.post.uri; }); createEffect(() => { const uri = props.uri; if (!uri) { setState(createPostPanelState()); return; } if (state.uri === uri && (state.loading || state.thread || state.error)) { return; } const nextRequestId = ++requestId; void loadThread(uri, nextRequestId); }); async function loadThread(uri: string, nextRequestId: number) { setState({ error: null, loading: true, thread: null, uri }); try { const payload = await FeedController.getPostThread(uri); if (nextRequestId !== requestId || props.uri !== uri) { return; } setState({ error: null, loading: false, thread: payload.thread, uri }); } catch (error) { if (nextRequestId !== requestId || props.uri !== uri) { return; } setState({ error: String(error), loading: false, thread: null, uri }); session.reportError(`Failed to open post: ${String(error)}`); } } return (

Post

{(parentUri) => ( Parent post )}
}> void interactions.toggleBookmark(post)} onLike={(post) => void interactions.toggleLike(post)} onOpenEngagement={(uri, tab) => void postNavigation.openPostEngagement(uri, tab)} onOpenPost={(uri) => void postNavigation.openPost(uri)} onRepost={(post) => void interactions.toggleRepost(post)} parentChain={parentChain()} repostPendingByUri={interactions.repostPendingByUri()} />
); } function ThreadState( props: { bookmarkPendingByUri: Record; error: string | null; focusedNode: ThreadViewPost | null; likePendingByUri: Record; loading: boolean; onBookmark: (post: PostView) => void; onLike: (post: PostView) => void; onOpenEngagement: (uri: string, tab: "likes" | "reposts" | "quotes") => void; onOpenPost: (uri: string) => void; onRepost: (post: PostView) => void; parentChain: ThreadViewPost[]; repostPendingByUri: Record; }, ) { return ( <>
{(message) => } {(focused) => (
{(parent) => (
props.onBookmark(parent.post)} onLike={() => props.onLike(parent.post)} onOpenEngagement={(tab) => props.onOpenEngagement(parent.post.uri, tab)} onOpenThread={(uri) => props.onOpenPost(uri)} onRepost={() => props.onRepost(parent.post)} post={parent.post} repostPending={!!props.repostPendingByUri[parent.post.uri]} showActions={false} />
)}
props.onBookmark(focused().post)} onLike={() => props.onLike(focused().post)} onOpenEngagement={(tab) => props.onOpenEngagement(focused().post.uri, tab)} onOpenThread={(uri) => props.onOpenPost(uri)} onRepost={() => props.onRepost(focused().post)} post={focused().post} repostPending={!!props.repostPendingByUri[focused().post.uri]} />
{(reply) => ( )}
)}
); } function ThreadReplies( props: { bookmarkPendingByUri: Record; likePendingByUri: Record; node: ThreadNode; onBookmark: (post: PostView) => void; onLike: (post: PostView) => void; onOpenEngagement: (uri: string, tab: "likes" | "reposts" | "quotes") => void; onOpenPost: (uri: string) => void; onRepost: (post: PostView) => void; repostPendingByUri: Record; }, ) { const threadNode = createMemo(() => (isThreadViewPost(props.node) ? props.node : null)); return ( {(current) => (
props.onBookmark(current().post)} onLike={() => props.onLike(current().post)} onOpenEngagement={(tab) => props.onOpenEngagement(current().post.uri, tab)} onOpenThread={(uri) => props.onOpenPost(uri)} onRepost={() => props.onRepost(current().post)} post={current().post} repostPending={!!props.repostPendingByUri[current().post.uri]} />
{(reply) => ( )}
)}
); } function PostPanelMessage(props: { body: string; title: string }) { return (

{props.title}

{props.body}

); } function SkeletonPostCard() { return (
); } function StateCard(props: { label: string; meta: string }) { return (

{props.label}

{props.meta}

); }