import { Component, createSignal, ErrorBoundary, type Ref, createSelector, Index, createMemo, } from "solid-js"; import { type mastodon } from "masto"; import { vibrate } from "../platform/hardware"; import { useDefaultSession } from "../masto/clients"; import { useHeroSource } from "../platform/anim"; import { HERO as BOTTOM_SHEET_HERO } from "../material/BottomSheet"; import { setCache as setTootBottomSheetCache } from "./TootBottomSheet"; import { useNavigate } from "@solidjs/router"; import RegularToot, { findElementActionable, findRootToot, } from "./RegularToot"; import cardStyle from "../material/cards.module.css"; import type { ThreadNode } from "../masto/timelines"; import { useNavigator } from "../platform/StackedRouter"; function positionTootInThread(index: number, threadLength: number) { if (index === 0) { return "top"; } else if (index === threadLength - 1) { return "bottom"; } return "middle"; } const TootList: Component<{ ref?: Ref; id?: string; threads: readonly string[]; onUnknownThread: (id: string) => ThreadNode[] | undefined; onChangeToot: (id: string, value: mastodon.v1.Status) => void; }> = (props) => { const session = useDefaultSession(); const heroSrc = useHeroSource(); const [expandedThreadId, setExpandedThreadId] = createSignal(); const {push} = useNavigator(); const onBookmark = async (status: mastodon.v1.Status) => { const client = session()?.client; if (!client) return; const result = await (status.bookmarked ? client.v1.statuses.$select(status.id).unbookmark() : client.v1.statuses.$select(status.id).bookmark()); props.onChangeToot(result.id, result); }; const toggleBoost = async (status: mastodon.v1.Status) => { const client = session()?.client; if (!client) return; vibrate(50); const rootStatus = status.reblog ? status.reblog : status; const reblogged = rootStatus.reblogged; if (status.reblog) { props.onChangeToot(status.id, { ...status, reblog: { ...status.reblog, reblogged: !reblogged }, }); } else { props.onChangeToot(status.id, { ...status, reblogged: !reblogged, }); } const result = reblogged ? await client.v1.statuses.$select(status.id).unreblog() : (await client.v1.statuses.$select(status.id).reblog()).reblog!; if (status.reblog) { props.onChangeToot(status.id, { ...status, reblog: result, }); } else { props.onChangeToot(status.id, result); } }; const toggleFavourite = async (status: mastodon.v1.Status) => { const client = session()?.client; if (!client) return; const ovalue = status.favourited; props.onChangeToot(status.id, { ...status, favourited: !ovalue }); const result = ovalue ? await client.v1.statuses.$select(status.id).unfavourite() : await client.v1.statuses.$select(status.id).favourite(); props.onChangeToot(status.id, result); }; const openFullScreenToot = ( toot: mastodon.v1.Status, srcElement?: HTMLElement, reply?: boolean, ) => { const p = session()?.account; if (!p) return; const inf = p.inf; if (!inf) { console.warn("no account info?"); return; } if (heroSrc) { heroSrc[1]((x) => ({ ...x, [BOTTOM_SHEET_HERO]: srcElement })); } const acct = `${inf.username}@${p.site}`; setTootBottomSheetCache(acct, toot); push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, { state: reply ? { tootReply: true, } : undefined, }); }; const onItemClick = ( status: mastodon.v1.Status, event: MouseEvent & { target: EventTarget; currentTarget: HTMLElement }, ) => { if (!(event.target instanceof HTMLElement)) { throw new Error("target is not an element"); } const actionableElement = findElementActionable( event.target, event.currentTarget, ); if (actionableElement && checkIsExpended(status)) { if (actionableElement.dataset.action === "acct") { event.stopPropagation(); const target = actionableElement as HTMLAnchorElement; const acct = encodeURIComponent( target.dataset.client || `@${new URL(target.href).origin}`, ); push(`/${acct}/profile/${target.dataset.acctId}`); return; } else { console.warn("unknown action", actionableElement.dataset.rel); } } else if ( event.target.parentElement && event.target.parentElement.tagName === "A" ) { return; } // else if (!actionableElement || !checkIsExpended(status) || ) if (status.id !== expandedThreadId()) { setExpandedThreadId((x) => (x ? undefined : status.id)); } else { openFullScreenToot(status, event.currentTarget as HTMLElement); } }; const checkIsExpendedId = createSelector(expandedThreadId); const checkIsExpended = (status: mastodon.v1.Status) => checkIsExpendedId(status.id); const reply = ( status: mastodon.v1.Status, event: { currentTarget: HTMLElement }, ) => { const element = findRootToot(event.currentTarget); openFullScreenToot(status, element, true); }; return ( { console.error(err); return

Oops: {String(err)}

; }} >
{(threadId, threadIdx) => { const thread = createMemo(() => props.onUnknownThread(threadId())?.reverse(), ); const threadLength = () => thread()?.length ?? 0; return ( {(threadNode, index) => { const status = () => threadNode().value; return ( 1 ? positionTootInThread(index, threadLength()) : undefined } class={cardStyle.card} evaluated={checkIsExpended(status())} actionable={checkIsExpended(status())} onBookmark={onBookmark} onRetoot={toggleBoost} onFavourite={toggleFavourite} onReply={reply} onClick={[onItemClick, status()]} /> ); }} ); }}
); }; export default TootList;