diff --git a/src/UnexpectedError.tsx b/src/UnexpectedError.tsx index 906d99d..4177758 100644 --- a/src/UnexpectedError.tsx +++ b/src/UnexpectedError.tsx @@ -49,11 +49,13 @@ const UnexpectedError: Component<{ error?: any }> = (props) => {

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 + You can restart the app to see if this guy is gone. If you meet this guy repeatly, please report to us.

- +
diff --git a/src/platform/StackedRouter.css b/src/platform/StackedRouter.css index 6b912c1..eac26ce 100644 --- a/src/platform/StackedRouter.css +++ b/src/platform/StackedRouter.css @@ -3,6 +3,8 @@ display: contents; max-width: 100vw; max-width: 100dvw; + + contain: layout; } dialog.StackedPage { diff --git a/src/platform/StackedRouter.tsx b/src/platform/StackedRouter.tsx index 792ec97..5bc6d8c 100644 --- a/src/platform/StackedRouter.tsx +++ b/src/platform/StackedRouter.tsx @@ -2,7 +2,6 @@ import { StaticRouter, type RouterProps } from "@solidjs/router"; import { Component, createContext, - createEffect, createRenderEffect, createUniqueId, Index, @@ -34,8 +33,33 @@ export type NewFrameOptions = (T extends undefined state?: T; } : { state: T }) & { + /** + * The new frame should replace the current frame. + */ replace?: boolean; + /** + * The animatedOpen phase of the life cycle. + * + * You can use this hook to animate the opening + * of the frame. In this phase, the frame content is created + * and is mounted to the document. + * + * You must return an {@link Animation}. This function must be + * without side effects. This phase is ended after the {@link Animation} + * finished. + */ animateOpen?: StackFrame["animateOpen"]; + /** + * The animatedClose phase of the life cycle. + * + * You can use this hook to animate the closing of the frame. + * In this phase, the frame content is still mounted in the + * document and will be unmounted after this phase. + * + * You must return an {@link Animation}. This function must be + * without side effects. This phase is ended after the + * {@link Animation} finished. + */ animateClose?: StackFrame["animateClose"]; }; @@ -53,6 +77,10 @@ export type Navigator> = { const NavigatorContext = /* @__PURE__ */ createContext(); +export function useMaybeNavigator() { + return useContext(NavigatorContext); +} + /** * Get the navigator of the {@link StackedRouter}. * @@ -63,7 +91,7 @@ const NavigatorContext = /* @__PURE__ */ createContext(); * navigator to the type you need. */ export function useNavigator() { - const navigator = useContext(NavigatorContext); + const navigator = useMaybeNavigator(); if (!navigator) { throw new TypeError("not in available scope of StackedRouter"); @@ -80,8 +108,12 @@ export type CurrentFrame = { const CurrentFrameContext = /* @__PURE__ */ createContext>>(); +export function useMaybeCurrentFrame() { + return useContext(CurrentFrameContext); +} + export function useCurrentFrame() { - const frame = useContext(CurrentFrameContext); + const frame = useMaybeCurrentFrame(); if (!frame) { throw new TypeError("not in available scope of StackedRouter"); @@ -90,6 +122,28 @@ export function useCurrentFrame() { return frame; } +/** + * Return an accessor of is current frame is suspended. + * + * A suspended frame is the one not on the top. "Suspended" + * is the description of a certain situtation, not in the life cycle + * of a frame. + */ +export function useMaybeIsFrameSuspended() { + const { frames } = useMaybeNavigator() || {}; + + if (typeof frames === "undefined") { + return () => false; + } + + const thisFrame = useCurrentFrame(); + + return () => { + const idx = thisFrame().index; + return frames.length - 1 > idx; + }; +} + function onDialogClick( onClose: () => void, event: MouseEvent & { currentTarget: HTMLDialogElement }, @@ -189,10 +243,10 @@ const StackedRouter: Component = (oprops) => { )! as HTMLDialogElement; const createAnimation = lastFrame.animateClose ?? animateClose; requestAnimationFrame(() => { - element.classList.add("animating") + element.classList.add("animating"); const animation = createAnimation(element); animation.addEventListener("finish", () => { - element.classList.remove("animating") + element.classList.remove("animating"); onlyPopFrame(depth); }); }); diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index 339e157..13b59d7 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -3,11 +3,9 @@ import { Show, onMount, type ParentComponent, - children, - Suspense, + createRenderEffect, } from "solid-js"; import { useDocumentTitle } from "../utils"; -import { type mastodon } from "masto"; import Scaffold from "../material/Scaffold"; import { AppBar, @@ -23,17 +21,11 @@ import ProfileMenuButton from "./ProfileMenuButton"; import Tabs from "../material/Tabs"; import Tab from "../material/Tab"; 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 { HeroSourceProvider, type HeroSource } from "../platform/anim"; -import { setCache as setTootBottomSheetCache } from "./TootBottomSheet"; import TrendTimelinePanel from "./TrendTimelinePanel"; import TimelinePanel from "./TimelinePanel"; import { useSessions } from "../masto/clients"; -import { useNavigator } from "../platform/StackedRouter"; const Home: ParentComponent = (props) => { let panelList: HTMLDivElement; @@ -43,29 +35,17 @@ const Home: ParentComponent = (props) => { const settings$ = useStore($settings); const profiles = useSessions(); - const profile = () => { - const all = profiles(); - if (all.length > 0) { - return all[0].account.inf; - } - }; const client = () => { const all = profiles(); return all?.[0]?.client; }; - const {pop, push} = useNavigator(); - const [heroSrc, setHeroSrc] = createSignal({}); - const [panelOffset, setPanelOffset] = createSignal(0); const prefetching = () => !settings$().prefetchTootsDisabled; - const [currentFocusOn, setCurrentFocusOn] = createSignal([]); const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [ number, number, ]); - const child = children(() => props.children); - let scrollEventLockReleased = true; const recalculateTabIndicator = () => { @@ -102,17 +82,17 @@ const Home: ParentComponent = (props) => { } }; + const requestRecalculateTabIndicator = () => { + if (scrollEventLockReleased) { + requestAnimationFrame(recalculateTabIndicator); + } + }; + + createRenderEffect(() => { + makeEventListener(window, "resize", requestRecalculateTabIndicator); + }); + onMount(() => { - makeEventListener(panelList, "scroll", () => { - if (scrollEventLockReleased) { - requestAnimationFrame(recalculateTabIndicator); - } - }); - makeEventListener(window, "resize", () => { - if (scrollEventLockReleased) { - requestAnimationFrame(recalculateTabIndicator); - } - }); requestAnimationFrame(recalculateTabIndicator); }); @@ -135,30 +115,6 @@ const Home: ParentComponent = (props) => { } }; - const openFullScreenToot = ( - toot: mastodon.v1.Status, - srcElement?: HTMLElement, - reply?: boolean, - ) => { - const p = profiles()[0]; - const inf = p.account.inf ?? profile(); - if (!inf) { - console.warn("no account info?"); - return; - } - setHeroSrc((x) => - Object.assign({}, x, { [BOTTOM_SHEET_HERO]: srcElement }), - ); - const acct = `${inf.username}@${p.account.site}`; - setTootBottomSheetCache(acct, toot); - push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, { - state: reply - ? { - tootReply: true, - } - : undefined, - }); - }; css` .tab-panel { @@ -209,7 +165,7 @@ const Home: ParentComponent = (props) => { class="responsive" sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }} > - + Home @@ -239,48 +195,40 @@ const Home: ParentComponent = (props) => { } > - - - -
-
-
- -
+ + +
+
+
+
-
-
- -
-
-
-
- -
-
-
- - - - pop(1)}> - {child()} - - - +
+
+ +
+
+
+
+ +
+
+
+
+
+
); diff --git a/src/timelines/PullDownToRefresh.tsx b/src/timelines/PullDownToRefresh.tsx index 10df2bf..7cbacdb 100644 --- a/src/timelines/PullDownToRefresh.tsx +++ b/src/timelines/PullDownToRefresh.tsx @@ -10,6 +10,7 @@ import { Refresh as RefreshIcon } from "@suid/icons-material"; import { CircularProgress } from "@suid/material"; import { makeEventListener } from "@solid-primitives/event-listener"; import { createVisibilityObserver } from "@solid-primitives/intersection-observer"; +import { useMaybeIsFrameSuspended } from "../platform/StackedRouter"; const PullDownToRefresh: Component<{ loading?: boolean; @@ -33,6 +34,7 @@ const PullDownToRefresh: Component<{ }); const rootVisible = obvx(() => rootElement); + const isFrameSuspended = useMaybeIsFrameSuspended() createEffect(() => { if (!rootVisible()) setPullDown(0); @@ -109,6 +111,9 @@ const PullDownToRefresh: Component<{ if (!rootVisible()) { return; } + if (isFrameSuspended()) { + return; + } const element = props.linkedElement; if (!element) return; makeEventListener(element, "wheel", handleLinkedWheel); @@ -159,6 +164,9 @@ const PullDownToRefresh: Component<{ if (!rootVisible()) { return; } + if (isFrameSuspended()) { + return; + } const element = props.linkedElement; if (!element) return; makeEventListener(element, "touchmove", handleTouch); diff --git a/src/timelines/TimelinePanel.tsx b/src/timelines/TimelinePanel.tsx index 150ca0e..7a4c4e1 100644 --- a/src/timelines/TimelinePanel.tsx +++ b/src/timelines/TimelinePanel.tsx @@ -20,12 +20,6 @@ const TimelinePanel: Component<{ client: mastodon.rest.Client; name: "home" | "public"; prefetch?: boolean; - - openFullScreenToot: ( - toot: mastodon.v1.Status, - srcElement?: HTMLElement, - reply?: boolean, - ) => void; }> = (props) => { const [scrollLinked, setScrollLinked] = createSignal(); diff --git a/src/timelines/TrendTimelinePanel.tsx b/src/timelines/TrendTimelinePanel.tsx index 1691ec8..9f0ba75 100644 --- a/src/timelines/TrendTimelinePanel.tsx +++ b/src/timelines/TrendTimelinePanel.tsx @@ -13,12 +13,6 @@ import TootList from "./TootList.jsx"; const TrendTimelinePanel: Component<{ client: mastodon.rest.Client; - - openFullScreenToot: ( - toot: mastodon.v1.Status, - srcElement?: HTMLElement, - reply?: boolean, - ) => void; }> = (props) => { const [scrollLinked, setScrollLinked] = createSignal(); const [tl, snapshot, { refetch: refetchTimeline }] = createTimelineSnapshot(