diff --git a/bun.lockb b/bun.lockb index 337f675..be11679 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.html b/index.html index b97ae88..2712433 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,6 @@ Tutu - diff --git a/package.json b/package.json index 32a9d10..54e32ca 100644 --- a/package.json +++ b/package.json @@ -16,22 +16,22 @@ "devDependencies": { "@suid/vite-plugin": "^0.2.0", "@types/hammerjs": "^2.0.45", - "postcss": "^8.4.41", - "prettier": "^3.3.3", - "typescript": "^5.5.4", - "vite": "^5.4.0", + "postcss": "^8.4.39", + "prettier": "^3.3.2", + "typescript": "^5.5.2", + "vite": "^5.3.2", "vite-plugin-package-version": "^1.1.0", - "vite-plugin-pwa": "^0.20.1", + "vite-plugin-pwa": "^0.20.0", "vite-plugin-solid": "^2.10.2", "vite-plugin-solid-styled": "^0.11.1", - "wrangler": "^3.70.0" + "wrangler": "^3.64.0" }, "dependencies": { "@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.26", + "@solid-primitives/resize-observer": "^2.0.25", "@solidjs/router": "^0.11.5", "@suid/icons-material": "^0.7.0", "@suid/material": "^0.16.0", @@ -40,10 +40,9 @@ "hammerjs": "^2.0.8", "masto": "^6.8.0", "nanostores": "^0.9.5", - "solid-js": "^1.8.20", + "solid-js": "^1.8.18", "solid-styled": "^0.11.1", - "stacktrace-js": "^2.0.2", - "web-animations-js": "^2.3.2" + "stacktrace-js": "^2.0.2" }, "packageManager": "bun@1.1.21" } diff --git a/src/App.tsx b/src/App.tsx index c3573a5..1522795 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,6 @@ const AccountMastodonOAuth2Callback = lazy( ); const TimelineHome = lazy(() => import("./timelines/Home.js")); const Settings = lazy(() => import("./settings/Settings.js")); -const TootBottomSheet = lazy(() => import("./timelines/TootBottomSheet.js")); const Routing: Component = () => { return ( @@ -30,7 +29,6 @@ const Routing: Component = () => { - @@ -55,7 +53,7 @@ const App: Component = () => { ); }); - const UnexpectedError = lazy(() => import("./UnexpectedError.js")); +const UnexpectedError = lazy(() => import("./UnexpectedError.js")) return ( ) { + return createResource( + client, + (client) => { + return client.v1.accounts.verifyCredentials(); + }, + { + name: "MastodonAccountProfile", + }, + ); +} + export function useSignedInProfiles() { const sessions = useSessions(); const [accessor, tools] = createResource(sessions, async (all) => { @@ -12,11 +24,11 @@ export function useSignedInProfiles() { }); return [ () => { - const value = accessor(); - if (!value) { + if (accessor.loading) { + accessor(); return sessions().map((x) => ({ ...x, inf: x.account.inf })); } - return value; + return accessor(); }, tools, ] as const; diff --git a/src/masto/timelines.ts b/src/masto/timelines.ts index 917ecf7..55d1276 100644 --- a/src/masto/timelines.ts +++ b/src/masto/timelines.ts @@ -14,49 +14,55 @@ type Timeline = { }; export function useTimeline(timeline: Accessor) { + let minId: string | undefined; + let maxId: string | undefined; let otl: Timeline | undefined; - let npager: mastodon.Paginator | undefined; - let opager: mastodon.Paginator | undefined; + const idSet = new Set(); const [snapshot, { refetch }] = createResource< - { - records: mastodon.v1.Status[]; - direction: "new" | "old"; - tlChanged: boolean; - }, + { records: mastodon.v1.Status[]; direction: "old" | "new" }, [Timeline], TimelineFetchTips | undefined >( () => [timeline()] as const, async ([tl], info) => { - let tlChanged = false; if (otl !== tl) { - console.debug("timeline reset"); - npager = opager = undefined; + minId = undefined; + maxId = undefined; + idSet.clear(); otl = tl; - tlChanged = true; } const direction = typeof info.refetching !== "boolean" - ? (info.refetching?.direction ?? "old") + ? info.refetching?.direction : "old"; + const pager = await tl.list( + direction === "old" + ? { + maxId: minId, + } + : { + minId: maxId, + }, + ); + const diff = pager.filter((x) => !idSet.has(x.id)); + for (const v of diff.map((x) => x.id)) { + idSet.add(v); + } if (direction === "old") { - if (!opager) { - opager = tl.list({}).setDirection("next"); + minId = pager[pager.length - 1]?.id; + if (!maxId && pager.length > 0) { + maxId = pager[0].id; } - const next = await opager.next(); return { - direction, - records: next.value ?? [], - end: next.done, - tlChanged, + direction: "old" as const, + records: diff, }; } else { - if (!npager) { - npager = tl.list({}).setDirection("prev"); + maxId = pager.length > 0 ? pager[0].id : undefined; + if (!minId && pager.length > 0) { + minId = pager[pager.length - 1]?.id; } - const next = await npager.next(); - const page = next.value ?? []; - return { direction, records: page, end: next.done, tlChanged }; + return { direction: "new" as const, records: diff }; } }, ); @@ -66,10 +72,7 @@ export function useTimeline(timeline: Accessor) { createEffect(() => { const shot = snapshot(); if (!shot) return; - const { direction, records, tlChanged } = shot; - if (tlChanged) { - setStore(() => []); - } + const { direction, records } = shot; if (direction == "new") { setStore((x) => [...records, ...x]); } else if (direction == "old") { diff --git a/src/masto/toot.ts b/src/masto/toot.ts index 2651577..85fd817 100644 --- a/src/masto/toot.ts +++ b/src/masto/toot.ts @@ -1,4 +1,3 @@ -import { cache } from "@solidjs/router"; import type { mastodon } from "masto"; import { createRenderEffect, createResource, type Accessor } from "solid-js"; diff --git a/src/material/BottomSheet.module.css b/src/material/BottomSheet.module.css index 237d0bd..e48ce13 100644 --- a/src/material/BottomSheet.module.css +++ b/src/material/BottomSheet.module.css @@ -11,14 +11,10 @@ border-radius: 2px; overscroll-behavior: contain; - &::backdrop { - background-color: black; - opacity: 0.5; - } - box-shadow: var(--tutu-shadow-e16); :global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child { + color: white; margin-left: -0.5em; margin-right: 24px; } @@ -34,9 +30,4 @@ max-height: 100%; } } - - &.animated { - position: absolute; - transform: none; - } } diff --git a/src/material/BottomSheet.tsx b/src/material/BottomSheet.tsx index b5b93cc..eeefcc5 100644 --- a/src/material/BottomSheet.tsx +++ b/src/material/BottomSheet.tsx @@ -1,115 +1,22 @@ -import { - createEffect, - createRenderEffect, - onCleanup, - onMount, - startTransition, - useTransition, - type ParentComponent, -} from "solid-js"; +import { createEffect, type ParentComponent } from "solid-js"; import styles from "./BottomSheet.module.css"; -import { useHeroSignal } from "../platform/anim"; export type BottomSheetProps = { open?: boolean; }; -export const HERO = Symbol("BottomSheet Hero Symbol"); - -function composeAnimationFrame( - { - top, - left, - height, - width, - }: Record<"top" | "left" | "height" | "width", number>, - x: Record, -) { - return { - top: `${top}px`, - left: `${left}px`, - height: `${height}px`, - width: `${width}px`, - ...x, - }; -} - -const MOVE_SPEED = 1400; // 1400px/s, bottom sheet is big and a bit heavier than small papers - const BottomSheet: ParentComponent = (props) => { let element: HTMLDialogElement; - let animation: Animation | undefined; - const hero = useHeroSignal(HERO); - - const [pending] = useTransition() createEffect(() => { if (props.open) { - if (!element.open && !pending()) { - animatedOpen(); + if (!element.open) { + element.showModal(); } } else { if (element.open) { - animatedClose(); - } - } - }); - - const animatedClose = () => { - const endRect = hero(); - if (endRect) { - const startRect = element.getBoundingClientRect(); - const animation = animateHero(startRect, endRect, element, true); - const onClose = () => { element.close(); - }; - animation.addEventListener("finish", onClose); - animation.addEventListener("cancel", onClose); - } else { - element.close(); - } - }; - - const animatedOpen = () => { - element.showModal(); - const startRect = hero(); - if (!startRect) return; - const endRect = element.getBoundingClientRect(); - animateHero(startRect, endRect, element); - }; - - const animateHero = ( - startRect: DOMRect, - endRect: DOMRect, - element: HTMLElement, - reserve?: boolean, - ) => { - const easing = "cubic-bezier(0.4, 0, 0.2, 1)"; - element.classList.add(styles.animated); - const distance = Math.sqrt( - Math.pow(Math.abs(startRect.top - endRect.top), 2) + - Math.pow(Math.abs(startRect.left - startRect.top), 2), - ); - const duration = (distance / MOVE_SPEED) * 1000; - animation = element.animate( - [ - composeAnimationFrame(startRect, { opacity: reserve ? 1 : 0.5 }), - composeAnimationFrame(endRect, { opacity: reserve ? 0.5 : 1 }), - ], - { easing, duration }, - ); - const onAnimationEnd = () => { - element.classList.remove(styles.animated); - animation = undefined; - }; - animation.addEventListener("finish", onAnimationEnd); - animation.addEventListener("cancel", onAnimationEnd); - return animation; - }; - - onCleanup(() => { - if (animation) { - animation.cancel(); + } } }); diff --git a/src/platform/anim.ts b/src/platform/anim.ts index 0a937e2..75446ef 100644 --- a/src/platform/anim.ts +++ b/src/platform/anim.ts @@ -1,49 +1,13 @@ -import { - createContext, - createRenderEffect, - createSignal, - untrack, - useContext, - type Accessor, - type Signal, -} from "solid-js"; +import { createContext, useContext, type Accessor } from "solid-js"; export type HeroSource = { - [key: string | symbol | number]: DOMRect | undefined; + [key: string | symbol | number]: HTMLElement | undefined; }; -const HeroSourceContext = createContext>(/* __@PURE__ */undefined); +const HeroSourceContext = createContext>(() => ({})); export const HeroSourceProvider = HeroSourceContext.Provider; -function useHeroSource() { +export function useHeroSource() { return useContext(HeroSourceContext); } - -/** - * Use hero value for the {@link key}. - */ -export function useHeroSignal( - key: string | symbol | number, -): Accessor { - const source = useHeroSource(); - if (source) { - const [get, set] = createSignal(); - - createRenderEffect(() => { - const value = source[0](); - if (value[key]) { - set(value[key]); - source[1]((x) => { - const cpy = Object.assign({}, x); - delete cpy[key]; - return cpy; - }); - } - }); - - return get; - } else { - return () => undefined; - } -} diff --git a/src/platform/host.ts b/src/platform/host.ts deleted file mode 100644 index f1a2e27..0000000 --- a/src/platform/host.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function isiOS() { - return [ - 'iPad Simulator', - 'iPhone Simulator', - 'iPod Simulator', - 'iPad', - 'iPhone', - 'iPod' - ].includes(navigator.platform) - // iPad on iOS 13 detection - || (navigator.userAgent.includes("Mac") && "ontouchend" in document) -} \ No newline at end of file diff --git a/src/platform/polyfills.ts b/src/platform/polyfills.ts deleted file mode 100644 index 6897af7..0000000 --- a/src/platform/polyfills.ts +++ /dev/null @@ -1,8 +0,0 @@ -//! This module has side effect. -//! It recommended to include the module by