Compare commits

..

4 commits

Author SHA1 Message Date
thislight
6fbd198021
UnexpectedError: restart the app instead of reload 2024-11-17 21:00:36 +08:00
thislight
3c50f150dc
PullDownToRefresh: adpats useMaybeIsFrameSuspended 2024-11-17 20:58:23 +08:00
thislight
169aa91e73
StackedRouter: add useMaybeIsFrameSuspended() 2024-11-17 20:57:56 +08:00
thislight
f0dadebfb6
Home: minor cleanup
- remove unused prop openFullScreenToot from
  TimelinePanel and TrendTimelinePanel
2024-11-17 20:27:49 +08:00
7 changed files with 117 additions and 115 deletions

View file

@ -49,11 +49,13 @@ const UnexpectedError: Component<{ error?: any }> = (props) => {
<h1>Oh, it is our fault.</h1> <h1>Oh, it is our fault.</h1>
<p>There is an unexpected error in our app, and it's not your fault.</p> <p>There is an unexpected error in our app, and it's not your fault.</p>
<p> <p>
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. repeatly, please report to us.
</p> </p>
<div> <div>
<Button onClick={() => window.location.reload()}>Reload</Button> <Button onClick={() => (window.location.replace("/"))}>
Restart App
</Button>
</div> </div>
<details> <details>
<summary> <summary>

View file

@ -3,6 +3,8 @@
display: contents; display: contents;
max-width: 100vw; max-width: 100vw;
max-width: 100dvw; max-width: 100dvw;
contain: layout;
} }
dialog.StackedPage { dialog.StackedPage {

View file

@ -2,7 +2,6 @@ import { StaticRouter, type RouterProps } from "@solidjs/router";
import { import {
Component, Component,
createContext, createContext,
createEffect,
createRenderEffect, createRenderEffect,
createUniqueId, createUniqueId,
Index, Index,
@ -34,8 +33,33 @@ export type NewFrameOptions<T> = (T extends undefined
state?: T; state?: T;
} }
: { state: T }) & { : { state: T }) & {
/**
* The new frame should replace the current frame.
*/
replace?: boolean; 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"]; 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"]; animateClose?: StackFrame["animateClose"];
}; };
@ -53,6 +77,10 @@ export type Navigator<PushGuide = Record<string, any>> = {
const NavigatorContext = /* @__PURE__ */ createContext<Navigator>(); const NavigatorContext = /* @__PURE__ */ createContext<Navigator>();
export function useMaybeNavigator() {
return useContext(NavigatorContext);
}
/** /**
* Get the navigator of the {@link StackedRouter}. * Get the navigator of the {@link StackedRouter}.
* *
@ -63,7 +91,7 @@ const NavigatorContext = /* @__PURE__ */ createContext<Navigator>();
* navigator to the type you need. * navigator to the type you need.
*/ */
export function useNavigator() { export function useNavigator() {
const navigator = useContext(NavigatorContext); const navigator = useMaybeNavigator();
if (!navigator) { if (!navigator) {
throw new TypeError("not in available scope of StackedRouter"); throw new TypeError("not in available scope of StackedRouter");
@ -80,8 +108,12 @@ export type CurrentFrame = {
const CurrentFrameContext = const CurrentFrameContext =
/* @__PURE__ */ createContext<Accessor<Readonly<CurrentFrame>>>(); /* @__PURE__ */ createContext<Accessor<Readonly<CurrentFrame>>>();
export function useMaybeCurrentFrame() {
return useContext(CurrentFrameContext);
}
export function useCurrentFrame() { export function useCurrentFrame() {
const frame = useContext(CurrentFrameContext); const frame = useMaybeCurrentFrame();
if (!frame) { if (!frame) {
throw new TypeError("not in available scope of StackedRouter"); throw new TypeError("not in available scope of StackedRouter");
@ -90,6 +122,28 @@ export function useCurrentFrame() {
return frame; 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( function onDialogClick(
onClose: () => void, onClose: () => void,
event: MouseEvent & { currentTarget: HTMLDialogElement }, event: MouseEvent & { currentTarget: HTMLDialogElement },
@ -189,10 +243,10 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
)! as HTMLDialogElement; )! as HTMLDialogElement;
const createAnimation = lastFrame.animateClose ?? animateClose; const createAnimation = lastFrame.animateClose ?? animateClose;
requestAnimationFrame(() => { requestAnimationFrame(() => {
element.classList.add("animating") element.classList.add("animating");
const animation = createAnimation(element); const animation = createAnimation(element);
animation.addEventListener("finish", () => { animation.addEventListener("finish", () => {
element.classList.remove("animating") element.classList.remove("animating");
onlyPopFrame(depth); onlyPopFrame(depth);
}); });
}); });

View file

@ -3,11 +3,9 @@ import {
Show, Show,
onMount, onMount,
type ParentComponent, type ParentComponent,
children, createRenderEffect,
Suspense,
} from "solid-js"; } from "solid-js";
import { useDocumentTitle } from "../utils"; import { useDocumentTitle } from "../utils";
import { type mastodon } from "masto";
import Scaffold from "../material/Scaffold"; import Scaffold from "../material/Scaffold";
import { import {
AppBar, AppBar,
@ -23,17 +21,11 @@ import ProfileMenuButton from "./ProfileMenuButton";
import Tabs from "../material/Tabs"; import Tabs from "../material/Tabs";
import Tab from "../material/Tab"; import Tab from "../material/Tab";
import { makeEventListener } from "@solid-primitives/event-listener"; import { makeEventListener } from "@solid-primitives/event-listener";
import BottomSheet, {
HERO as BOTTOM_SHEET_HERO,
} from "../material/BottomSheet";
import { $settings } from "../settings/stores"; import { $settings } from "../settings/stores";
import { useStore } from "@nanostores/solid"; import { useStore } from "@nanostores/solid";
import { HeroSourceProvider, type HeroSource } from "../platform/anim";
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
import TrendTimelinePanel from "./TrendTimelinePanel"; import TrendTimelinePanel from "./TrendTimelinePanel";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import { useSessions } from "../masto/clients"; import { useSessions } from "../masto/clients";
import { useNavigator } from "../platform/StackedRouter";
const Home: ParentComponent = (props) => { const Home: ParentComponent = (props) => {
let panelList: HTMLDivElement; let panelList: HTMLDivElement;
@ -43,29 +35,17 @@ const Home: ParentComponent = (props) => {
const settings$ = useStore($settings); const settings$ = useStore($settings);
const profiles = useSessions(); const profiles = useSessions();
const profile = () => {
const all = profiles();
if (all.length > 0) {
return all[0].account.inf;
}
};
const client = () => { const client = () => {
const all = profiles(); const all = profiles();
return all?.[0]?.client; return all?.[0]?.client;
}; };
const {pop, push} = useNavigator();
const [heroSrc, setHeroSrc] = createSignal<HeroSource>({});
const [panelOffset, setPanelOffset] = createSignal(0);
const prefetching = () => !settings$().prefetchTootsDisabled; const prefetching = () => !settings$().prefetchTootsDisabled;
const [currentFocusOn, setCurrentFocusOn] = createSignal<HTMLElement[]>([]);
const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [ const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [
number, number,
number, number,
]); ]);
const child = children(() => props.children);
let scrollEventLockReleased = true; let scrollEventLockReleased = true;
const recalculateTabIndicator = () => { const recalculateTabIndicator = () => {
@ -102,17 +82,17 @@ const Home: ParentComponent = (props) => {
} }
}; };
const requestRecalculateTabIndicator = () => {
if (scrollEventLockReleased) {
requestAnimationFrame(recalculateTabIndicator);
}
};
createRenderEffect(() => {
makeEventListener(window, "resize", requestRecalculateTabIndicator);
});
onMount(() => { onMount(() => {
makeEventListener(panelList, "scroll", () => {
if (scrollEventLockReleased) {
requestAnimationFrame(recalculateTabIndicator);
}
});
makeEventListener(window, "resize", () => {
if (scrollEventLockReleased) {
requestAnimationFrame(recalculateTabIndicator);
}
});
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` css`
.tab-panel { .tab-panel {
@ -209,7 +165,7 @@ const Home: ParentComponent = (props) => {
class="responsive" class="responsive"
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }} sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
> >
<Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}> <Tabs>
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}> <Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
Home Home
</Tab> </Tab>
@ -239,48 +195,40 @@ const Home: ParentComponent = (props) => {
</AppBar> </AppBar>
} }
> >
<HeroSourceProvider value={[heroSrc, setHeroSrc]}> <TimeSourceProvider value={now}>
<TimeSourceProvider value={now}> <Show when={!!client()}>
<Show when={!!client()}> <div
<div class="panel-list" ref={panelList!}> class="panel-list"
<div class="tab-panel"> ref={panelList!}
<div> onScroll={requestRecalculateTabIndicator}
<TimelinePanel >
client={client()} <div class="tab-panel">
name="home" <div>
prefetch={prefetching()} <TimelinePanel
openFullScreenToot={openFullScreenToot} client={client()}
/> name="home"
</div> prefetch={prefetching()}
/>
</div> </div>
<div class="tab-panel">
<div>
<TrendTimelinePanel
client={client()}
openFullScreenToot={openFullScreenToot}
/>
</div>
</div>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="public"
prefetch={prefetching()}
openFullScreenToot={openFullScreenToot}
/>
</div>
</div>
<div></div>
</div> </div>
</Show> <div class="tab-panel">
</TimeSourceProvider> <div>
<Suspense> <TrendTimelinePanel client={client()} />
<BottomSheet open={!!child()} onClose={() => pop(1)}> </div>
{child()} </div>
</BottomSheet> <div class="tab-panel">
</Suspense> <div>
</HeroSourceProvider> <TimelinePanel
client={client()}
name="public"
prefetch={prefetching()}
/>
</div>
</div>
<div></div>
</div>
</Show>
</TimeSourceProvider>
</Scaffold> </Scaffold>
</> </>
); );

View file

@ -10,6 +10,7 @@ import { Refresh as RefreshIcon } from "@suid/icons-material";
import { CircularProgress } from "@suid/material"; import { CircularProgress } from "@suid/material";
import { makeEventListener } from "@solid-primitives/event-listener"; import { makeEventListener } from "@solid-primitives/event-listener";
import { createVisibilityObserver } from "@solid-primitives/intersection-observer"; import { createVisibilityObserver } from "@solid-primitives/intersection-observer";
import { useMaybeIsFrameSuspended } from "../platform/StackedRouter";
const PullDownToRefresh: Component<{ const PullDownToRefresh: Component<{
loading?: boolean; loading?: boolean;
@ -33,6 +34,7 @@ const PullDownToRefresh: Component<{
}); });
const rootVisible = obvx(() => rootElement); const rootVisible = obvx(() => rootElement);
const isFrameSuspended = useMaybeIsFrameSuspended()
createEffect(() => { createEffect(() => {
if (!rootVisible()) setPullDown(0); if (!rootVisible()) setPullDown(0);
@ -109,6 +111,9 @@ const PullDownToRefresh: Component<{
if (!rootVisible()) { if (!rootVisible()) {
return; return;
} }
if (isFrameSuspended()) {
return;
}
const element = props.linkedElement; const element = props.linkedElement;
if (!element) return; if (!element) return;
makeEventListener(element, "wheel", handleLinkedWheel); makeEventListener(element, "wheel", handleLinkedWheel);
@ -159,6 +164,9 @@ const PullDownToRefresh: Component<{
if (!rootVisible()) { if (!rootVisible()) {
return; return;
} }
if (isFrameSuspended()) {
return;
}
const element = props.linkedElement; const element = props.linkedElement;
if (!element) return; if (!element) return;
makeEventListener(element, "touchmove", handleTouch); makeEventListener(element, "touchmove", handleTouch);

View file

@ -20,12 +20,6 @@ const TimelinePanel: Component<{
client: mastodon.rest.Client; client: mastodon.rest.Client;
name: "home" | "public"; name: "home" | "public";
prefetch?: boolean; prefetch?: boolean;
openFullScreenToot: (
toot: mastodon.v1.Status,
srcElement?: HTMLElement,
reply?: boolean,
) => void;
}> = (props) => { }> = (props) => {
const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>(); const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();

View file

@ -13,12 +13,6 @@ import TootList from "./TootList.jsx";
const TrendTimelinePanel: Component<{ const TrendTimelinePanel: Component<{
client: mastodon.rest.Client; client: mastodon.rest.Client;
openFullScreenToot: (
toot: mastodon.v1.Status,
srcElement?: HTMLElement,
reply?: boolean,
) => void;
}> = (props) => { }> = (props) => {
const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>(); const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
const [tl, snapshot, { refetch: refetchTimeline }] = createTimelineSnapshot( const [tl, snapshot, { refetch: refetchTimeline }] = createTimelineSnapshot(