import { Component, createSignal, ErrorBoundary, type Ref, createSelector, Index, createMemo, For, createUniqueId, } from "solid-js"; import { type mastodon } from "masto"; import { vibrate } from "~platform/hardware"; import { useDefaultSession } from "../masto/clients"; import { setCache as setTootBottomSheetCache } from "./TootBottomSheet"; import RegularToot, { findElementActionable, findRootToot, TootEnvProvider, } from "./RegularToot"; import cardStyle from "~material/cards.module.css"; import type { ThreadNode } from "../masto/timelines"; import { useNavigator } from "~platform/StackedRouter"; import { ANIM_CURVE_STD } from "~material/theme"; import { useItemSelection } from "./toots/ItemSelectionProvider"; function durationOf(rect0: DOMRect, rect1: DOMRect) { const distancelt = Math.sqrt( Math.pow(Math.abs(rect0.top - rect1.top), 2) + Math.pow(Math.abs(rect0.left - rect1.left), 2), ); const distancerb = Math.sqrt( Math.pow(Math.abs(rect0.bottom - rect1.bottom), 2) + Math.pow(Math.abs(rect0.right - rect1.right), 2), ); const distance = distancelt + distancerb; const duration = distance / 1.6; return duration; } function positionTootInThread(index: number, threadLength: number) { if (index === 0) { return "top"; } else if (index === threadLength - 1) { return "bottom"; } return "middle"; } /** * Full-feature toot list. */ 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 [isExpanded, setExpanded] = useItemSelection(); 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; } const acct = `${inf.username}@${p.site}`; setTootBottomSheetCache(acct, toot); push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, { animateOpen(element) { const rect0 = srcElement.getBoundingClientRect(); // the start rect const rect1 = element.getBoundingClientRect(); // the end rect const duration = durationOf(rect0, rect1); const keyframes = { top: [`${rect0.top}px`, `${rect1.top}px`], bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`], left: [`${rect0.left}px`, `${rect1.left}px`], right: [`${rect0.right}px`, `${rect1.right}px`], height: [`${rect0.height}px`, `${rect1.height}px`], margin: 0, }; srcElement.style.visibility = "hidden"; const animation = element.animate(keyframes, { duration, easing: ANIM_CURVE_STD, }); return animation; }, animateClose(element) { const rect0 = element.getBoundingClientRect(); // the start rect const rect1 = srcElement.getBoundingClientRect(); // the end rect const duration = durationOf(rect0, rect1); const keyframes = { top: [`${rect0.top}px`, `${rect1.top}px`], bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`], left: [`${rect0.left}px`, `${rect1.left}px`], right: [`${rect0.right}px`, `${rect1.right}px`], height: [`${rect0.height}px`, `${rect1.height}px`], margin: 0, }; srcElement.style.visibility = ""; const animation = element.animate(keyframes, { duration, easing: ANIM_CURVE_STD, }); return animation; }, }); }; 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 && isExpanded(event.currentTarget.id)) { 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 (!isExpanded(event.currentTarget.id)) { setExpanded(event.currentTarget.id); } else { openFullScreenToot(status, event.currentTarget as HTMLElement); } }; const reply = ( status: mastodon.v1.Status, event: { currentTarget: HTMLElement }, ) => { const element = findRootToot(event.currentTarget); openFullScreenToot(status, element, true); }; const vote = async (status: mastodon.v1.Status, votes: readonly number[]) => { const client = session()?.client; if (!client) return; const toot = status.reblog ?? status; if (!toot.poll) return; const npoll = await client.v1.polls.$select(toot.poll.id).votes.create({ choices: votes, }); if (status.reblog) { props.onChangeToot(status.id, { ...status, reblog: { ...status.reblog, poll: npoll, }, }); } else { props.onChangeToot(status.id, { ...status, poll: npoll, }); } }; 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; const id = createUniqueId(); return ( 1 ? positionTootInThread(index, threadLength()) : undefined } class={cardStyle.card} evaluated={isExpanded(id)} actionable={isExpanded(id)} onClick={[onItemClick, status()]} /> ); }} ); }}
); }; export default TootList;