import { Component, For, createSignal, ErrorBoundary, type Ref, createSelector, } from "solid-js"; import { type mastodon } from "masto"; import { vibrate } from "../platform/hardware"; import Thread from "./Thread.jsx"; 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 { findElementActionable } from "./RegularToot"; const TootList: Component<{ ref?: Ref<HTMLDivElement>; id?: string; threads: readonly string[]; onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined; onChangeToot: (id: string, value: mastodon.v1.Status) => void; }> = (props) => { const session = useDefaultSession(); const heroSrc = useHeroSource(); const [expandedThreadId, setExpandedThreadId] = createSignal<string>(); const navigate = useNavigate(); const onBookmark = async ( client: mastodon.rest.Client, status: mastodon.v1.Status, ) => { 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 ( client: mastodon.rest.Client, status: mastodon.v1.Status, ) => { 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; 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); navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, { state: reply ? { tootReply: true, } : undefined, }); }; const onItemClick = ( status: mastodon.v1.Status, event: MouseEvent & { target: HTMLElement; currentTarget: HTMLElement }, ) => { 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}`, ); navigate(`/${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) || <rel is not one of known action>) 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 onReply = ( { status }: { status: mastodon.v1.Status }, element: HTMLElement, ) => { openFullScreenToot(status, element, true); }; const getPath = (itemId: string) => { return props .onUnknownThread(itemId)! .reverse() .map((x) => x.value); }; return ( <ErrorBoundary fallback={(err, reset) => { return <p>Oops: {String(err)}</p>; }} > <div ref={props.ref} id={props.id} class="toot-list"> <For each={props.threads}> {(itemId) => { return ( <Thread toots={getPath(itemId)} onBoost={toggleBoost} onBookmark={onBookmark} onReply={onReply} onFavourite={toggleFavourite} client={session()?.client!} isExpended={checkIsExpended} onItemClick={onItemClick} /> ); }} </For> </div> </ErrorBoundary> ); }; export default TootList;