diff --git a/bun.lockb b/bun.lockb index 3dc8a47..25e9097 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index e5a7f53..19f3a48 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "@solid-primitives/event-listener": "^2.3.3", "@solid-primitives/i18n": "^2.1.1", "@solid-primitives/intersection-observer": "^2.1.6", - "@solid-primitives/map": "^0.4.13", "@solid-primitives/resize-observer": "^2.0.26", "@solidjs/router": "^0.14.5", "@suid/icons-material": "^0.8.0", diff --git a/src/UnexpectedError.tsx b/src/UnexpectedError.tsx index aa7f468..38e3c70 100644 --- a/src/UnexpectedError.tsx +++ b/src/UnexpectedError.tsx @@ -1,59 +1,40 @@ -import { Button } from "@suid/material"; -import { Component, createResource } from "solid-js"; -import { css } from "solid-styled"; +import { Button } from '@suid/material'; +import {Component, createResource} from 'solid-js' +import { css } from 'solid-styled'; -const UnexpectedError: Component<{ error?: any }> = (props) => { - const [errorMsg] = createResource( - () => props.error, - async (err) => { - if (err instanceof Error) { - const mod = await import("stacktrace-js"); - try { - const stacktrace = await mod.fromError(err); - const strackMsg = stacktrace - .map( - (entry) => - `${entry.functionName ?? ""}@${entry.fileName}:(${entry.lineNumber}:${entry.columnNumber})`, - ) - .join("\n"); - return `${err.name}: ${err.message}\n${strackMsg}`; - } catch (reason) { - return `\n${reason}`; - } - } +const UnexpectedError: Component<{error?: any}> = (props) => { - return err.toString(); - }, - ); + const [errorMsg] = createResource(() => props.error, async (err) => { + if (err instanceof Error) { + const mod = await import('stacktrace-js') + const stacktrace = await mod.fromError(err) + const strackMsg = stacktrace.map(entry => `${entry.functionName ?? ""}@${entry.fileName}:(${entry.lineNumber}:${entry.columnNumber})`).join('\n') + return `${err.name}: ${err.message}\n${strackMsg}` + } + + return err.toString() + }) css` - main { - padding: calc(var(--safe-area-inset-top) + 20px) - calc(var(--safe-area-inset-right) + 20px) - calc(var(--safe-area-inset-bottom) + 20px) - calc(var(--safe-area-inset-left) + 20px); - } - `; + main { + padding: calc(var(--safe-area-inset-top) + 20px) calc(var(--safe-area-inset-right) + 20px) calc(var(--safe-area-inset-bottom) + 20px) calc(var(--safe-area-inset-left) + 20px); + } + ` - return ( -
-

Oh, it is our fault.

-

There is an unexpected error in our app, and it's not your fault.

-

- You can reload to see if this guy is gone. If you meet this guy - repeatly, please report to us. -

-
- -
-
- - {errorMsg.loading ? "Generating " : " "}Technical Infomation - -
{errorMsg()}
-
-
- ); -}; + return
+

Oh, it is our fault.

+

There is an unexpected error in our app, and it's not your fault.

+

You can reload to see if this guy is gone. If you meet this guy repeatly, please report to us.

+
+ +
+
+ {errorMsg.loading ? 'Generating ' : " "}Technical Infomation (Bring to us if you report the problem) +
+        {errorMsg()}
+      
+
+
+} export default UnexpectedError; diff --git a/src/masto/timelines.ts b/src/masto/timelines.ts index 8eb9c81..2e49145 100644 --- a/src/masto/timelines.ts +++ b/src/masto/timelines.ts @@ -1,258 +1,111 @@ -import { ReactiveMap } from "@solid-primitives/map"; import { type mastodon } from "masto"; -import { - Accessor, - batch, - catchError, - createEffect, - createResource, - untrack, - type ResourceFetcherInfo, -} from "solid-js"; +import { Accessor, createEffect, createResource } from "solid-js"; import { createStore } from "solid-js/store"; +type TimelineFetchTips = { + direction?: "new" | "old"; +}; + type Timeline = { list(params: { - /** Return results older than this ID. */ - readonly maxId?: string; - /** Return results newer than this ID. */ - readonly sinceId?: string; - /** Get a list of items with ID greater than this value excluding this ID */ - readonly minId?: string; - /** Maximum number of results to return per page. Defaults to 40. NOTE: Pagination is done with the Link header from the response. */ readonly limit?: number; }): mastodon.Paginator; }; -export function createTimelineSnapshot( +export function useTimeline( timeline: Accessor, - limit: Accessor, + cfg?: { + /** + * Use full refresh mode. This mode ignores paging, it will refetch the specified number + * of toots at every refetch(). + */ + fullRefresh?: number; + }, ) { - const [shot, { refetch }] = createResource( - () => [timeline(), limit()] as const, - async ([tl, limit]) => { - const ls = await tl.list({ limit }).next(); - return ls.value?.map((x) => [x]) ?? []; + let otl: Timeline | undefined; + let npager: mastodon.Paginator | undefined; + let opager: mastodon.Paginator | undefined; + const [snapshot, { refetch }] = createResource< + { + records: mastodon.v1.Status[]; + direction: "new" | "old" | "items"; + tlChanged: boolean; + }, + [Timeline], + TimelineFetchTips | undefined + >( + () => [timeline()] as const, + async ([tl], info) => { + let tlChanged = false; + if (otl !== tl) { + npager = opager = undefined; + otl = tl; + tlChanged = true; + } + const fullRefresh = cfg?.fullRefresh; + if (typeof fullRefresh !== "undefined") { + const records = await tl + .list({ + limit: fullRefresh, + }) + .next(); + return { + direction: "items", + records: records.value ?? [], + end: records.done, + tlChanged, + }; + } + const direction = + typeof info.refetching !== "boolean" + ? (info.refetching?.direction ?? "old") + : "old"; + if (direction === "old") { + if (!opager) { + opager = tl.list({}).setDirection("next"); + } + const next = await opager.next(); + return { + direction, + records: next.value ?? [], + end: next.done, + tlChanged, + }; + } else { + if (!npager) { + npager = tl.list({}).setDirection("prev"); + } + const next = await npager.next(); + const page = next.value ?? []; + return { direction, records: page, end: next.done, tlChanged }; + } }, ); - const [snapshot, setSnapshot] = createStore([] as mastodon.v1.Status[][]); + const [store, setStore] = createStore([] as mastodon.v1.Status[]); createEffect(() => { - const nls = catchError(shot, (e) => console.error(e)); - if (!nls) return; - const ols = Array.from(snapshot); - // The algorithm below assumes the snapshot is not changing - for (let i = 0; i < nls.length; i++) { - if (i >= ols.length) { - setSnapshot(i, nls[i]); - } else { - if (nls[i].length !== ols[i].length) { - setSnapshot(i, nls[i]); - } else { - const oth = ols[i], - nth = nls[i]; - for (let j = 0; j < oth.length; j++) { - const ost = oth[j], - nst = nth[j]; - for (const key of Object.keys( - nst, - ) as unknown as (keyof mastodon.v1.Status)[]) { - if (ost[key] !== nst[key]) { - setSnapshot(i, j, key, nst[key]); - } - } - } - } - } + const shot = snapshot(); + if (!shot) return; + const { direction, records, tlChanged } = shot; + if (tlChanged) { + setStore(() => []); + } + if (direction === "new") { + setStore((x) => [...records, ...x]); + } else if (direction === "old") { + setStore((x) => [...x, ...records]); + } else if (direction === "items") { + setStore(() => records); } }); return [ + store, snapshot, - shot, { refetch, - mutate: setSnapshot, + mutate: setStore, }, ] as const; } - -export type TimelineFetchDirection = mastodon.Direction; - -export type TimelineChunk = { - tl: Timeline; - rebuilt: boolean; - chunk: readonly mastodon.v1.Status[]; - done?: boolean; - direction: TimelineFetchDirection; - limit: number; -}; - -type TreeNode = { - parent?: TreeNode; - value: T; - children?: TreeNode[]; -}; - -/** Collect the path of a node for the root. - * The first element is the node itself, the last element is the root. - */ -function collectPath(node: TreeNode) { - const path = [node] as TreeNode[]; - let current = node; - while (current.parent) { - path.push(current.parent); - current = current.parent; - } - return path; -} - -function createTimelineChunk( - timeline: Accessor, - limit: Accessor, -) { - let vpMaxId: string | undefined, vpMinId: string | undefined; - - const fetchExtendingPage = async ( - tl: Timeline, - direction: TimelineFetchDirection, - limit: number, - ) => { - switch (direction) { - case "next": { - const page = await tl - .list({ limit, sinceId: vpMaxId }) - .setDirection(direction) - .next(); - if ((page.value?.length ?? 0) > 0) { - vpMaxId = page.value![0].id; - } - return page; - } - - case "prev": { - const page = await tl - .list({ limit, maxId: vpMinId }) - .setDirection(direction) - .next(); - if ((page.value?.length ?? 0) > 0) { - vpMinId = page.value![page.value!.length - 1].id; - } - return page; - } - } - }; - - return createResource( - () => [timeline(), limit()] as const, - async ( - [tl, limit], - info: ResourceFetcherInfo< - Readonly, - TimelineFetchDirection - >, - ) => { - const direction = - typeof info.refetching === "boolean" ? "prev" : info.refetching; - const rebuildTimeline = tl !== info.value?.tl; - if (rebuildTimeline) { - vpMaxId = undefined; - vpMinId = undefined; - } - const posts = await fetchExtendingPage(tl, direction, limit); - return { - tl, - rebuilt: rebuildTimeline, - chunk: posts.value ?? [], - done: posts.done, - direction, - limit, - }; - }, - ); -} - -export function createTimeline( - timeline: Accessor, - limit: Accessor, -) { - const lookup = new ReactiveMap>(); - const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); - - const [chunk, { refetch }] = createTimelineChunk(timeline, limit); - - createEffect(() => { - const chk = catchError(chunk, (e) => console.error(e)); - if (!chk) { - return; - } - - if (chk.rebuilt) { - lookup.clear(); - setThreads([]); - } - - const existence = [] as boolean[]; - - 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))!; - 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; - } - } - } - - const nThreadIds = chk.chunk - .filter((x, i) => !existence[i]) - .map((x) => x.id); - - batch(() => { - if (chk.direction === "prev") { - setThreads((threads) => [...threads, ...nThreadIds]); - } else if (chk.direction === "next") { - setThreads((threads) => [...nThreadIds, ...threads]); - } - - setThreads((threads) => - threads.filter((id) => (lookup.get(id)!.children?.length ?? 0) === 0), - ); - }); - }); - - return [ - { - list: threads, - get(id: string) { - return lookup.get(id); - }, - getPath(id: string) { - const node = lookup.get(id); - if (!node) return; - return collectPath(node); - }, - set(id: string, value: mastodon.v1.Status) { - const node = untrack(() => lookup.get(id)); - if (!node) return; - node.value = value; - lookup.set(id, node); - }, - }, - chunk, - { refetch }, - ] as const; -} diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index 91ba685..8321872 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -1,16 +1,26 @@ import { + Component, + For, + onCleanup, createSignal, Show, + untrack, onMount, type ParentComponent, children, Suspense, + Match, + Switch as JsSwitch, + ErrorBoundary, } from "solid-js"; import { useDocumentTitle } from "../utils"; import { type mastodon } from "masto"; import Scaffold from "../material/Scaffold"; import { AppBar, + Button, + Fab, + LinearProgress, ListItemSecondaryAction, ListItemText, MenuItem, @@ -19,21 +29,205 @@ import { } from "@suid/material"; import { css } from "solid-styled"; import { TimeSourceProvider, createTimeSource } from "../platform/timesrc"; +import TootThread from "./TootThread.js"; import ProfileMenuButton from "./ProfileMenuButton"; import Tabs from "../material/Tabs"; import Tab from "../material/Tab"; +import { Create as CreateTootIcon } from "@suid/icons-material"; +import { useTimeline } from "../masto/timelines"; import { makeEventListener } from "@solid-primitives/event-listener"; import BottomSheet, { HERO as BOTTOM_SHEET_HERO, } from "../material/BottomSheet"; import { $settings } from "../settings/stores"; import { useStore } from "@nanostores/solid"; +import { vibrate } from "../platform/hardware"; +import PullDownToRefresh from "./PullDownToRefresh"; import { HeroSourceProvider, type HeroSource } from "../platform/anim"; import { useNavigate } from "@solidjs/router"; import { useSignedInProfiles } from "../masto/acct"; import { setCache as setTootBottomSheetCache } from "./TootBottomSheet"; -import TrendTimelinePanel from "./TrendTimelinePanel"; -import TimelinePanel from "./TimelinePanel"; +import TootComposer from "./TootComposer"; + +const TimelinePanel: Component<{ + client: mastodon.rest.Client; + name: "home" | "public" | "trends"; + prefetch?: boolean; + fullRefetch?: number; + + openFullScreenToot: ( + toot: mastodon.v1.Status, + srcElement?: HTMLElement, + reply?: boolean, + ) => void; +}> = (props) => { + const [scrollLinked, setScrollLinked] = createSignal(); + const [ + timeline, + snapshot, + { refetch: refetchTimeline, mutate: mutateTimeline }, + ] = useTimeline( + () => + props.name !== "trends" + ? props.client.v1.timelines[props.name] + : props.client.v1.trends.statuses, + { fullRefresh: props.fullRefetch }, + ); + const [expandedThreadId, setExpandedThreadId] = createSignal(); + const [typing, setTyping] = createSignal(false); + + const tlEndObserver = new IntersectionObserver(() => { + if (untrack(() => props.prefetch) && !snapshot.loading) + refetchTimeline({ direction: "old" }); + }); + + onCleanup(() => tlEndObserver.disconnect()); + + const onBookmark = async ( + index: number, + 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()); + mutateTimeline((o) => { + o[index] = result; + return o; + }); + }; + + const onBoost = async ( + index: number, + client: mastodon.rest.Client, + status: mastodon.v1.Status, + ) => { + const reblogged = status.reblog + ? status.reblog.reblogged + : status.reblogged; + vibrate(50); + mutateTimeline(index, (x) => { + if (x.reblog) { + x.reblog = { ...x.reblog, reblogged: !reblogged }; + return Object.assign({}, x); + } else { + return Object.assign({}, x, { + reblogged: !reblogged, + }); + } + }); + const result = reblogged + ? await client.v1.statuses.$select(status.id).unreblog() + : (await client.v1.statuses.$select(status.id).reblog()).reblog!; + mutateTimeline((o) => { + Object.assign(o[index].reblog ?? o[index], { + reblogged: result.reblogged, + reblogsCount: result.reblogsCount, + }); + return o; + }); + }; + + return ( + { + return

Oops: {String(err)}

; + }} + > + refetchTimeline({ direction: "new" })} + /> +
+ setTimeout(() => { + setScrollLinked(e.parentElement!); + }, 0) + } + > + + refetchTimeline({ direction: "new" })} + /> + + + {(item, index) => { + let element: HTMLElement | undefined; + return ( + onBoost(index(), ...args)} + onBookmark={(...args) => onBookmark(index(), ...args)} + onReply={(client, status) => + props.openFullScreenToot(status, element, true) + } + client={props.client} + expanded={item.id === expandedThreadId() ? 1 : 0} + onExpandChange={(x) => { + setTyping(false) + if (item.id !== expandedThreadId()) { + setExpandedThreadId((x) => (x ? undefined : item.id)); + } else if (x === 2) { + props.openFullScreenToot(item, element); + } + }} + /> + ); + }} + +
+ +
tlEndObserver.observe(e)}>
+ +
+ +
+
+
+ + + + + + + + +
+
+ ); +}; const Home: ParentComponent = (props) => { let panelList: HTMLDivElement; @@ -146,9 +340,7 @@ const Home: ParentComponent = (props) => { console.warn("no account info?"); return; } - setHeroSrc((x) => - Object.assign({}, x, { [BOTTOM_SHEET_HERO]: srcElement }), - ); + setHeroSrc((x) => Object.assign({}, x, { [BOTTOM_SHEET_HERO]: srcElement })); const acct = `${inf.username}@${p.account.site}`; setTootBottomSheetCache(acct, toot); navigate(`/${encodeURIComponent(acct)}/${toot.id}`, { @@ -247,10 +439,12 @@ const Home: ParentComponent = (props) => {
-
@@ -270,9 +464,7 @@ const Home: ParentComponent = (props) => { - navigate(-1)}> - {child()} - + {child()} diff --git a/src/timelines/MediaAttachmentGrid.tsx b/src/timelines/MediaAttachmentGrid.tsx index e22a767..567e67c 100644 --- a/src/timelines/MediaAttachmentGrid.tsx +++ b/src/timelines/MediaAttachmentGrid.tsx @@ -66,7 +66,7 @@ const MediaAttachmentGrid: Component<{ css` .attachments { - column-count: ${columnCount().toString()}; + column-count: ${columnCount.toString()}; } `; return ( diff --git a/src/timelines/PullDownToRefresh.tsx b/src/timelines/PullDownToRefresh.tsx index 1c80829..55859e5 100644 --- a/src/timelines/PullDownToRefresh.tsx +++ b/src/timelines/PullDownToRefresh.tsx @@ -52,7 +52,7 @@ const PullDownToRefresh: Component<{ let lts = -1; let ds = 0; let holding = false; - const K = 20; + const K = 10; const updatePullDown = (ts: number) => { released = false; try { @@ -60,9 +60,8 @@ const PullDownToRefresh: Component<{ const dt = lts !== -1 ? ts - lts : 1 / 60; const vspring = holding ? 0 : K * x * dt; v = ds / dt - vspring; - const final = Math.max(Math.min(x + v * dt, stopPos()), 0); - setPullDown(final); + setPullDown(Math.max(Math.min(x + v * dt, stopPos()), 0)); if (Math.abs(x) > 1 || Math.abs(v) > 1) { requestAnimationFrame(updatePullDown); @@ -70,6 +69,15 @@ const PullDownToRefresh: Component<{ v = 0; lts = -1; } + + if ( + !holding && + untrack(pullDown) >= stopPos() && + !props.loading && + props.onRefresh + ) { + setTimeout(props.onRefresh, 0); + } } finally { ds = 0; released = true; @@ -81,11 +89,6 @@ const PullDownToRefresh: Component<{ const onWheelNotUpdated = () => { wheelTimeout = undefined; holding = false; - - if (released) { - released = false; - requestAnimationFrame(updatePullDown); - } }; const handleLinkedWheel = (event: WheelEvent) => { @@ -94,18 +97,11 @@ const PullDownToRefresh: Component<{ const d = untrack(pullDown); if (d > 1) event.preventDefault(); ds = -(event.deltaY / window.devicePixelRatio / 2); + holding = d < stopPos(); if (wheelTimeout) { clearTimeout(wheelTimeout); } - if (d >= stopPos() && !props.loading) { - props.onRefresh?.(); - - holding = false; - wheelTimeout = undefined; - } else { - holding = true; - wheelTimeout = setTimeout(onWheelNotUpdated, 200); - } + wheelTimeout = setTimeout(onWheelNotUpdated, 200); if (released) { released = false; @@ -155,8 +151,12 @@ const PullDownToRefresh: Component<{ lastTouchId = undefined; lastTouchScreenY = 0; holding = false; - if (untrack(pullDown) >= stopPos() && !props.loading) { - props.onRefresh?.(); + if ( + untrack(indicatorOfsY) >= stopPos() && + !props.loading && + props.onRefresh + ) { + setTimeout(props.onRefresh, 0); } else { if (released) { released = false; @@ -203,7 +203,9 @@ const PullDownToRefresh: Component<{ background-color: var(--tutu-color-surface); > :global(.refresh-icon) { - transform: rotate(${`${(indicatorOfsY() / 160 / 2).toString()}turn`}); + transform: rotate( + ${`${((indicatorOfsY() / 160) * 180).toString()}deg`} + ); will-change: transform; } diff --git a/src/timelines/RegularToot.tsx b/src/timelines/RegularToot.tsx index 2042618..1c9eb60 100644 --- a/src/timelines/RegularToot.tsx +++ b/src/timelines/RegularToot.tsx @@ -111,17 +111,13 @@ type TootActionGroupProps = { onRetoot?: (value: T) => void; onFavourite?: (value: T) => void; onBookmark?: (value: T) => void; - onReply?: ( - value: T, - event: MouseEvent & { currentTarget: HTMLButtonElement }, - ) => void; + onReply?: (value: T) => void; }; type TootCardProps = { status: mastodon.v1.Status; actionable?: boolean; evaluated?: boolean; - thread?: "top" | "bottom" | "middle"; } & TootActionGroupProps & JSX.HTMLElementTags["article"]; @@ -129,40 +125,19 @@ function isolatedCallback(e: MouseEvent) { e.stopPropagation(); } -export function findRootToot(element: HTMLElement) { - let current: HTMLElement | null = element; - while (current && !current.classList.contains(tootStyle.toot)) { - current = current.parentElement; - } - if (!current) { - throw Error( - `the element must be placed under a element with ${tootStyle.toot}`, - ); - } - return current; -} - function TootActionGroup( props: TootActionGroupProps & { value: T }, ) { - let actGrpElement: HTMLDivElement; const toot = () => props.value; return ( -
- - - - +
+ - - - - - -
- - ); -}; - -export default TimelinePanel; diff --git a/src/timelines/TootBottomSheet.tsx b/src/timelines/TootBottomSheet.tsx index 0081c97..dc1bc4e 100644 --- a/src/timelines/TootBottomSheet.tsx +++ b/src/timelines/TootBottomSheet.tsx @@ -62,6 +62,9 @@ const TootBottomSheet: Component = (props) => { } ); }; + const profile = () => { + return session().account; + }; const pushedCount = () => { return location.state?.tootBottomSheetPushedCount || 0; diff --git a/src/timelines/TrendTimelinePanel.tsx b/src/timelines/TrendTimelinePanel.tsx deleted file mode 100644 index a2370e8..0000000 --- a/src/timelines/TrendTimelinePanel.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { - Component, - For, - onCleanup, - createSignal, - untrack, - 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"; - -const TrendTimelinePanel: Component<{ - client: mastodon.rest.Client; - prefetch?: boolean; - - openFullScreenToot: ( - toot: mastodon.v1.Status, - srcElement?: HTMLElement, - reply?: boolean, - ) => void; -}> = (props) => { - const [scrollLinked, setScrollLinked] = createSignal(); - const [ - timeline, - snapshot, - { refetch: refetchTimeline, mutate: mutateTimeline }, - ] = createTimelineSnapshot( - () => props.client.v1.trends.statuses, - () => 120, - ); - const [expandedId, setExpandedId] = createSignal(); - - const tlEndObserver = new IntersectionObserver(() => { - if (untrack(() => props.prefetch) && !snapshot.loading) - refetchTimeline(); - }); - - onCleanup(() => tlEndObserver.disconnect()); - - const isExpandedId = createSelector(expandedId); - - const isExpanded = (st: mastodon.v1.Status) => isExpandedId(st.id); - - const onBookmark = async ( - index: number, - 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()); - mutateTimeline((o) => { - o![index] = [result]; - return o; - }); - }; - - const onBoost = async ( - index: number, - client: mastodon.rest.Client, - status: mastodon.v1.Status, - ) => { - const reblogged = status.reblog - ? status.reblog.reblogged - : status.reblogged; - vibrate(50); - mutateTimeline(index, (th) => { - const x = th[0]; - if (x.reblog) { - x.reblog = { ...x.reblog, reblogged: !reblogged }; - return [Object.assign({}, x)]; - } else { - return [ - Object.assign({}, x, { - reblogged: !reblogged, - }), - ]; - } - }); - const result = reblogged - ? await client.v1.statuses.$select(status.id).unreblog() - : (await client.v1.statuses.$select(status.id).reblog()).reblog!; - mutateTimeline(index, (th) => { - Object.assign(th[0].reblog ?? th[0], { - reblogged: result.reblogged, - reblogsCount: result.reblogsCount, - }); - return th; - }); - }; - - return ( - { - return

Oops: {String(err)}

; - }} - > - refetchTimeline({ direction: "new" })} - /> -
- setTimeout(() => { - setScrollLinked(e.parentElement!); - }, 0) - } - > - - {(item, index) => { - let element: HTMLElement | undefined; - return ( - onBoost(index(), ...args)} - onBookmark={(...args) => onBookmark(index(), ...args)} - onReply={(client, status) => - props.openFullScreenToot(status, element, true) - } - client={props.client} - isExpended={isExpanded} - onItemClick={(x) => { - if (x.id !== expandedId()) { - setExpandedId((o) => (o ? undefined : x.id)); - } else { - props.openFullScreenToot(x, element); - } - }} - /> - ); - }} - -
- -
tlEndObserver.observe(e)}>
-
- - -

{`Oops: ${snapshot.error}`}

- - -
- - - -
-
-
- ); -}; - -export default TrendTimelinePanel;