diff --git a/bun.lockb b/bun.lockb index be11679..16c0c62 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 54e32ca..953d8c5 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "@nanostores/persistent": "^0.9.1", "@nanostores/solid": "^0.4.2", "@solid-primitives/event-listener": "^2.3.3", - "@solid-primitives/intersection-observer": "^2.1.6", "@solid-primitives/resize-observer": "^2.0.25", "@solidjs/router": "^0.11.5", "@suid/icons-material": "^0.7.0", diff --git a/src/masto/timelines.ts b/src/masto/timelines.ts index 55d1276..91e1d84 100644 --- a/src/masto/timelines.ts +++ b/src/masto/timelines.ts @@ -1,6 +1,5 @@ import { type mastodon } from "masto"; -import { Accessor, createEffect, createResource, createSignal } from "solid-js"; -import { createStore } from "solid-js/store"; +import { Accessor, createResource, createSignal } from "solid-js"; type TimelineFetchTips = { direction?: "new" | "old"; @@ -18,8 +17,8 @@ export function useTimeline(timeline: Accessor) { let maxId: string | undefined; let otl: Timeline | undefined; const idSet = new Set(); - const [snapshot, { refetch }] = createResource< - { records: mastodon.v1.Status[]; direction: "old" | "new" }, + return createResource< + mastodon.v1.Status[], [Timeline], TimelineFetchTips | undefined >( @@ -29,6 +28,7 @@ export function useTimeline(timeline: Accessor) { minId = undefined; maxId = undefined; idSet.clear(); + info.value = []; otl = tl; } const direction = @@ -44,6 +44,7 @@ export function useTimeline(timeline: Accessor) { minId: maxId, }, ); + const old = info.value || []; const diff = pager.filter((x) => !idSet.has(x.id)); for (const v of diff.map((x) => x.id)) { idSet.add(v); @@ -53,39 +54,20 @@ export function useTimeline(timeline: Accessor) { if (!maxId && pager.length > 0) { maxId = pager[0].id; } - return { - direction: "old" as const, - records: diff, - }; + return [...old, ...diff]; } else { maxId = pager.length > 0 ? pager[0].id : undefined; if (!minId && pager.length > 0) { minId = pager[pager.length - 1]?.id; } - return { direction: "new" as const, records: diff }; + return [...diff, ...old]; } }, - ); - - const [store, setStore] = createStore([] as mastodon.v1.Status[]); - - createEffect(() => { - const shot = snapshot(); - if (!shot) return; - const { direction, records } = shot; - if (direction == "new") { - setStore((x) => [...records, ...x]); - } else if (direction == "old") { - setStore((x) => [...x, ...records]); - } - }); - - return [ - store, - snapshot, { - refetch, - mutate: setStore, + initialValue: [], + storage(init) { + return createSignal(init, { equals: false }); + }, }, - ] as const; + ); } diff --git a/src/platform/hardware.ts b/src/platform/hardware.ts deleted file mode 100644 index 2d1d0d6..0000000 --- a/src/platform/hardware.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function vibrate(pattern: number | number[]) { - if (typeof navigator.vibrate !== "undefined") { - return navigator.vibrate(pattern); - } - return false; -} diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index 2330354..44682e1 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -37,27 +37,21 @@ import { makeEventListener } from "@solid-primitives/event-listener"; import BottomSheet from "../material/BottomSheet"; import { $settings } from "../settings/stores"; import { useStore } from "@nanostores/solid"; -import { vibrate } from "../platform/hardware"; -import PullDownToRefresh from "./PullDownToRefresh"; const TimelinePanel: Component<{ client: mastodon.rest.Client; name: "home" | "public" | "trends"; prefetch?: boolean; }> = (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, - ); + const [timeline, { refetch: refetchTimeline, mutate: mutateTimeline }] = + useTimeline(() => + props.name !== "trends" + ? props.client.v1.timelines[props.name] + : props.client.v1.trends.statuses, + ); const tlEndObserver = new IntersectionObserver(() => { - if (untrack(() => props.prefetch) && !snapshot.loading) + if (untrack(() => props.prefetch) && !timeline.loading) refetchTimeline({ direction: "old" }); }); @@ -82,19 +76,12 @@ const TimelinePanel: Component<{ 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 reblogged = false; + mutateTimeline((o) => { + Object.assign(o[index].reblog ?? o[index], { + reblogged: !reblogged, + }); + return o; }); const result = reblogged ? await client.v1.statuses.$select(status.id).unreblog() @@ -110,19 +97,8 @@ const TimelinePanel: Component<{ return ( <> - refetchTimeline({ direction: "new" })} - /> -
- setTimeout(() => { - setScrollLinked(e.parentElement!); - }, 0) - } - > - +
+ {(item, index) => { return (
tlEndObserver.observe(e)}>
- +
- +
- +
{ overflow: visible auto; max-width: 560px; height: 100%; - padding: 0 16px; + padding: 40px 16px; scroll-snap-align: center; - overscroll-behavior-block: none; @media (max-width: 600px) { padding: 0; diff --git a/src/timelines/PullDownToRefresh.tsx b/src/timelines/PullDownToRefresh.tsx deleted file mode 100644 index 3aa7195..0000000 --- a/src/timelines/PullDownToRefresh.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { - createEffect, - createRenderEffect, - createSignal, - onCleanup, - Show, - untrack, - type Component, - type Signal, -} from "solid-js"; -import { css } from "solid-styled"; -import { Refresh as RefreshIcon } from "@suid/icons-material"; -import { CircularProgress } from "@suid/material"; -import { - createEventListener, - makeEventListener, -} from "@solid-primitives/event-listener"; -import { - createViewportObserver, - createVisibilityObserver, -} from "@solid-primitives/intersection-observer"; - -const PullDownToRefresh: Component<{ - loading?: boolean; - linkedElement?: HTMLElement; - onRefresh?: () => void; -}> = (props) => { - let rootElement: HTMLDivElement; - const [pullDown, setPullDown] = createSignal(0); - - const pullDownDistance = () => { - if (props.loading) { - return 140; - } - return Math.max(Math.min(160, pullDown()), 0); - }; - - const obvx = createVisibilityObserver({ - threshold: 0.0001, - }); - - const rootVisible = obvx(() => rootElement); - - let released = true; - let v = 0; - let lts = -1; - let ds = 0; - let holding = false; - const M = 1; - const K = -10; - const D = -10; - const updatePullDown = (ts: number) => { - released = false; - try { - const x = untrack(pullDown); - const dt = lts !== -1 ? ts - lts : 1 / 60; - const fs = (ds / Math.pow(dt, 2)) * M; - const fh = (x + ds) * K + D * v; - const f = fs + fh; - const a = f / M; - v += a * dt; - if (holding && v < 0) { - v = 0 - } - setPullDown(x + v * dt); - if (Math.abs(x) > 1 || Math.abs(v) > 1) { - requestAnimationFrame(updatePullDown); - } else { - v = 0; - lts = -1; - } - if (!holding && untrack(pullDownDistance) >= 160 && !props.loading && props.onRefresh) { - setTimeout(props.onRefresh, 0) - } - } finally { - ds = 0; - released = true; - } - }; - const handleLinkedWheel = (event: WheelEvent) => { - const scrollTop = (event.target as HTMLElement).scrollTop; - if (scrollTop >= 0 && scrollTop < 1) { - ds = -(event.deltaY / window.devicePixelRatio / 4); - if (released) { - released = false; - requestAnimationFrame(updatePullDown); - } - } - }; - - createEffect((cleanup?: () => void) => { - if (!rootVisible()) { - return; - } - cleanup?.(); - const element = props.linkedElement; - if (!element) return; - return makeEventListener(element, "wheel", handleLinkedWheel); - }); - - let lastTouchId: number | undefined = undefined; - let lastTouchScreenY = 0; - const handleTouch = (event: TouchEvent) => { - if (event.targetTouches.length > 1) { - lastTouchId = 0; - lastTouchScreenY; - return; - } - const item = event.targetTouches.item(0)!; - if (lastTouchId && item.identifier !== lastTouchId) { - lastTouchId = undefined; - lastTouchScreenY = 0; - return; - } - holding = true; - if (lastTouchScreenY !== 0) { - ds = item.screenY - lastTouchScreenY; - } - lastTouchScreenY = item.screenY; - if (released) { - released = false; - requestAnimationFrame(updatePullDown); - } - }; - - const handleTouchEnd = () => { - lastTouchId = undefined; - lastTouchScreenY = 0; - holding = false; - if (untrack(pullDownDistance) >= 160 && !props.loading && props.onRefresh) { - setTimeout(props.onRefresh, 0) - } - }; - - createEffect((cleanup?: () => void) => { - if (!rootVisible()) { - return; - } - cleanup?.(); - const element = props.linkedElement; - if (!element) return; - const cleanup0 = makeEventListener(element, "touchmove", handleTouch); - const cleanup1 = makeEventListener(element, "touchend", handleTouchEnd); - return () => (cleanup0(), cleanup1()); - }); - - css` - .pull-down { - width: 100%; - display: flex; - justify-content: center; - margin-top: -2rem; - height: calc(1px + 2rem); - } - - .indicator { - display: inline-flex; - justify-content: center; - align-items: center; - box-shadow: ${props.loading - ? "var(--tutu-shadow-e12)" - : "var(--tutu-shadow-e1)"}; - border-radius: 50%; - aspect-ratio: 1/1; - width: 2rem; - color: var(--tutu-color-primary); - transform: translateY(${`${pullDownDistance() - 2}px`}); - will-change: transform; - z-index: var(--tutu-zidx-nav); - background-color: var(--tutu-color-surface); - - > :global(.refresh-icon) { - transform: rotate( - ${`${((pullDownDistance() / 160) * 180).toString()}deg`} - ); - will-change: transform; - } - - > :global(.refresh-indicator) { - width: 1.5rem; - height: 1.5rem; - aspect-ratio: 1/1; - } - } - `; - return ( -
- - } - > - - - -
- ); -}; - -export default PullDownToRefresh;