diff --git a/src/masto/timelines.ts b/src/masto/timelines.ts index f7bb5a3..556bc83 100644 --- a/src/masto/timelines.ts +++ b/src/masto/timelines.ts @@ -18,9 +18,9 @@ type Timeline = { type TimelineParamsOf = T extends Timeline ? P : never; -export type ThreadNode = TreeNode; - -function createControlsForLookup(lookup: ReactiveMap) { +function createControlsForLookup( + lookup: ReactiveMap>, +) { return { get(id: string) { return lookup.get(id); @@ -37,7 +37,7 @@ function createControlsForLookup(lookup: ReactiveMap) { set(id: string, value: mastodon.v1.Status) { const node = untrack(() => lookup.get(id)); if (!node) return; - lookup.set(id, { ...node, value }); + lookup.set(id, {...node, value}); }, }; } @@ -45,7 +45,7 @@ function createControlsForLookup(lookup: ReactiveMap) { export function createTimelineControlsForArray( status: () => mastodon.v1.Status[] | undefined, ): TimelineControls { - const lookup = new ReactiveMap(); + const lookup = new ReactiveMap>(); const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); @@ -55,24 +55,22 @@ export function createTimelineControlsForArray( }); if (!nls) return; - batch(() => { - setThreads([]); - lookup.clear(); + setThreads([]); + lookup.clear(); - for (const status of nls) { - lookup.set(status.id, { - value: status, - }); - } - }); + const existence = [] as boolean[]; - untrack(() => { - for (const status of nls) { - const node = lookup.get(status.id)!; - const parent = status.inReplyToId - ? lookup.get(status.inReplyToId) - : undefined; + for (const [idx, status] of nls.entries()) { + existence[idx] = !!untrack(() => lookup.get(status.id)); + lookup.set(status.id, { + value: status, + }); + } + for (const status of nls) { + const node = untrack(() => lookup.get(status.id))!; + if (status.inReplyToId) { + const parent = lookup.get(status.inReplyToId); if (parent) { const children = parent.children ?? []; if (!children.find((x) => x.value.id == status.id)) { @@ -82,13 +80,12 @@ export function createTimelineControlsForArray( node.parent = parent; } } - }); + } - const newThreads = untrack(() => - nls - .map((x) => x.id) - .filter((id) => (lookup.get(id)!.children?.length ?? 0) === 0), - ); + const newThreads = nls + .filter((x, i) => !existence[i]) + .map((x) => x.id) + .filter((id) => (lookup.get(id)!.children?.length ?? 0) === 0); setThreads(newThreads); }); @@ -272,34 +269,32 @@ export function createTimeline< return; } + if (chk.rebuilt) { + lookup.clear(); + setThreads([]); + } + const existence = [] as boolean[]; - batch(() => { - if (chk.rebuilt) { - lookup.clear(); - setThreads([]); - } - - for (const [idx, status] of chk.chunk.entries()) { - existence[idx] = !!untrack(() => lookup.get(status.id)); - lookup.set(status.id, { - value: status, - }); - } - }); + for (const [idx, status] of chk.chunk.entries()) { + existence[idx] = !!untrack(() => lookup.get(status.id)); + lookup.set(status.id, { + value: status, + }); + } for (const status of chk.chunk) { const node = untrack(() => lookup.get(status.id))!; - const parent = untrack(() => - status.inReplyToId ? lookup.get(status.inReplyToId) : undefined, - ); - if (parent) { - const children = parent.children ?? []; - if (!children.find((x) => x.value.id == status.id)) { - children.push(node); + if (status.inReplyToId) { + const parent = lookup.get(status.inReplyToId); + if (parent) { + const children = parent.children ?? []; + if (!children.find((x) => x.value.id == status.id)) { + children.push(node); + } + parent.children = children; + node.parent = parent; } - parent.children = children; - node.parent = parent; } } diff --git a/src/timelines/Thread.tsx b/src/timelines/Thread.tsx new file mode 100644 index 0000000..034c126 --- /dev/null +++ b/src/timelines/Thread.tsx @@ -0,0 +1,90 @@ +import type { mastodon } from "masto"; +import { Index, type Component, type Ref } from "solid-js"; +import RegularToot, { findRootToot } from "./RegularToot"; +import cardStyle from "../material/cards.module.css"; +import { css } from "solid-styled"; + +type TootActionTarget = { + client: mastodon.rest.Client; + status: mastodon.v1.Status; +}; + +type TootActions = { + onBoost(client: mastodon.rest.Client, status: mastodon.v1.Status): void; + onBookmark(client: mastodon.rest.Client, status: mastodon.v1.Status): void; + onReply(target: TootActionTarget, element: HTMLElement): void; + onFavourite(status: mastodon.v1.Status): void +}; + +type ThreadProps = { + ref?: Ref; + client: mastodon.rest.Client; + toots: readonly mastodon.v1.Status[]; + isExpended: (status: mastodon.v1.Status) => boolean; + + onItemClick(status: mastodon.v1.Status, event: MouseEvent): void; +} & TootActions; + +const Thread: Component = (props) => { + const boost = (status: mastodon.v1.Status) => { + props.onBoost(props.client, status); + }; + + const bookmark = (status: mastodon.v1.Status) => { + props.onBookmark(props.client, status); + }; + + const reply = ( + status: mastodon.v1.Status, + event: MouseEvent & { currentTarget: HTMLElement }, + ) => { + const element = findRootToot(event.currentTarget); + props.onReply({ client: props.client, status }, element); + }; + + const threading = () => props.toots.length > 1; + + const posThread = (index: number) => { + if (!threading()) return; + + if (index === 0) { + return "top"; + } else if (index === props.toots.length - 1) { + return "bottom"; + } + return "middle"; + }; + + css` + .thread { + user-select: none; + cursor: pointer; + } + `; + return ( +
+ + {(status, index) => { + return ( + bookmark(s)} + onRetoot={(s) => boost(s)} + onFavourite={props.onFavourite} + onReply={reply} + onClick={[props.onItemClick, status()]} + /> + ); + }} + +
+ ); +}; + +export default Thread; diff --git a/src/timelines/TootBottomSheet.tsx b/src/timelines/TootBottomSheet.tsx index c2b29e3..4cfb16f 100644 --- a/src/timelines/TootBottomSheet.tsx +++ b/src/timelines/TootBottomSheet.tsx @@ -58,10 +58,9 @@ const TootBottomSheet: Component = (props) => { }, ); - const toot = () => - catchError(remoteToot, (error) => { - console.error(error); - }) ?? getCache(acctText(), params.id); + const toot = () => catchError(remoteToot, (error) => { + console.error(error) + }) ?? getCache(acctText(), params.id); createEffect((lastTootId?: string) => { const tootId = toot()?.id; diff --git a/src/timelines/TootList.tsx b/src/timelines/TootList.tsx index 48a7836..30304d9 100644 --- a/src/timelines/TootList.tsx +++ b/src/timelines/TootList.tsx @@ -5,38 +5,22 @@ import { ErrorBoundary, type Ref, createSelector, - Index, - createMemo, } 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 RegularToot, { - findElementActionable, - findRootToot, -} from "./RegularToot"; -import cardStyle from "../material/cards.module.css"; -import type { ReactiveMap } from "@solid-primitives/map"; -import type { ThreadNode } from "../masto/timelines"; - -function positionTootInThread(index: number, threadLength: number) { - if (index === 0) { - return "top"; - } else if (index === threadLength - 1) { - return "bottom"; - } - return "middle"; -} +import { findElementActionable } from "./RegularToot"; const TootList: Component<{ ref?: Ref; id?: string; threads: readonly string[]; - onUnknownThread: (id: string) => ThreadNode[] | undefined; + onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined; onChangeToot: (id: string, value: mastodon.v1.Status) => void; }> = (props) => { const session = useDefaultSession(); @@ -44,20 +28,20 @@ const TootList: Component<{ const [expandedThreadId, setExpandedThreadId] = createSignal(); const navigate = useNavigate(); - const onBookmark = async (status: mastodon.v1.Status) => { - const client = session()?.client; - if (!client) return; - + 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 (status: mastodon.v1.Status) => { - const client = session()?.client; - if (!client) return; - + const toggleBoost = async ( + client: mastodon.rest.Client, + status: mastodon.v1.Status, + ) => { vibrate(50); const rootStatus = status.reblog ? status.reblog : status; const reblogged = rootStatus.reblogged; @@ -73,7 +57,7 @@ const TootList: Component<{ }); } - /* const result = reblogged + const result = reblogged ? await client.v1.statuses.$select(status.id).unreblog() : (await client.v1.statuses.$select(status.id).reblog()).reblog!; @@ -84,15 +68,13 @@ const TootList: Component<{ }); } 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(); @@ -128,11 +110,8 @@ const TootList: Component<{ const onItemClick = ( status: mastodon.v1.Status, - event: MouseEvent & { target: EventTarget; currentTarget: HTMLElement }, + event: MouseEvent & { target: HTMLElement; currentTarget: HTMLElement }, ) => { - if (!(event.target instanceof HTMLElement)) { - throw new Error("target is not an element"); - } const actionableElement = findElementActionable( event.target, event.currentTarget, @@ -174,14 +153,20 @@ const TootList: Component<{ const checkIsExpended = (status: mastodon.v1.Status) => checkIsExpendedId(status.id); - const reply = ( - status: mastodon.v1.Status, - event: { currentTarget: HTMLElement }, + const onReply = ( + { status }: { status: mastodon.v1.Status }, + element: HTMLElement, ) => { - const element = findRootToot(event.currentTarget); openFullScreenToot(status, element, true); }; + const getPath = (itemId: string) => { + return props + .onUnknownThread(itemId)! + .reverse() + .map((x) => x.value); + }; + return ( { @@ -189,46 +174,22 @@ const TootList: Component<{ }} >
- - {(threadId, threadIdx) => { - const thread = createMemo(() => - props.onUnknownThread(threadId())?.reverse(), - ); - - const threadLength = () => thread()?.length ?? 0; - + + {(itemId) => { 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()]} - /> - ); - }} - + ); }} - +
); diff --git a/src/timelines/TrendTimelinePanel.tsx b/src/timelines/TrendTimelinePanel.tsx index 1691ec8..e34edcf 100644 --- a/src/timelines/TrendTimelinePanel.tsx +++ b/src/timelines/TrendTimelinePanel.tsx @@ -1,14 +1,18 @@ import { Component, + For, createSignal, Match, Switch as JsSwitch, ErrorBoundary, + createSelector, } from "solid-js"; import { type mastodon } from "masto"; import { Button } from "@suid/material"; import { createTimelineSnapshot } from "../masto/timelines.js"; +import { vibrate } from "../platform/hardware.js"; import PullDownToRefresh from "./PullDownToRefresh.jsx"; +import Thread from "./Thread.jsx"; import TootList from "./TootList.jsx"; const TrendTimelinePanel: Component<{ @@ -21,10 +25,11 @@ const TrendTimelinePanel: Component<{ ) => void; }> = (props) => { const [scrollLinked, setScrollLinked] = createSignal(); - const [tl, snapshot, { refetch: refetchTimeline }] = createTimelineSnapshot( - () => props.client.v1.trends.statuses, - () => ({ limit: 120 }), - ); + const [timeline, snapshot, { refetch: refetchTimeline }] = + createTimelineSnapshot( + () => props.client.v1.trends.statuses, + () => ({ limit: 120 }), + ); return (