Compare commits

..

No commits in common. "6fbd198021b637e7c207f0c5bc12c6b82fcad1ac" and "6879eb52928c07a945f4977a22cfd150780910b2" have entirely different histories.

7 changed files with 113 additions and 115 deletions

View file

@ -49,13 +49,11 @@ 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 restart the app to see if this guy is gone. If you meet this guy You can reload 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.replace("/"))}> <Button onClick={() => window.location.reload()}>Reload</Button>
Restart App
</Button>
</div> </div>
<details> <details>
<summary> <summary>

View file

@ -3,8 +3,6 @@
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,6 +2,7 @@ import { StaticRouter, type RouterProps } from "@solidjs/router";
import { import {
Component, Component,
createContext, createContext,
createEffect,
createRenderEffect, createRenderEffect,
createUniqueId, createUniqueId,
Index, Index,
@ -33,33 +34,8 @@ 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"];
}; };
@ -77,10 +53,6 @@ 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}.
* *
@ -91,7 +63,7 @@ export function useMaybeNavigator() {
* navigator to the type you need. * navigator to the type you need.
*/ */
export function useNavigator() { export function useNavigator() {
const navigator = useMaybeNavigator(); const navigator = useContext(NavigatorContext);
if (!navigator) { if (!navigator) {
throw new TypeError("not in available scope of StackedRouter"); throw new TypeError("not in available scope of StackedRouter");
@ -108,12 +80,8 @@ 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 = useMaybeCurrentFrame(); const frame = useContext(CurrentFrameContext);
if (!frame) { if (!frame) {
throw new TypeError("not in available scope of StackedRouter"); throw new TypeError("not in available scope of StackedRouter");
@ -122,28 +90,6 @@ 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 },
@ -243,10 +189,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,9 +3,11 @@ import {
Show, Show,
onMount, onMount,
type ParentComponent, type ParentComponent,
createRenderEffect, children,
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,
@ -21,11 +23,17 @@ 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;
@ -35,17 +43,29 @@ 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 = () => {
@ -82,17 +102,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);
}); });
@ -115,6 +135,30 @@ 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 {
@ -165,7 +209,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> <Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}> <Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
Home Home
</Tab> </Tab>
@ -195,40 +239,48 @@ const Home: ParentComponent = (props) => {
</AppBar> </AppBar>
} }
> >
<TimeSourceProvider value={now}> <HeroSourceProvider value={[heroSrc, setHeroSrc]}>
<Show when={!!client()}> <TimeSourceProvider value={now}>
<div <Show when={!!client()}>
class="panel-list" <div class="panel-list" ref={panelList!}>
ref={panelList!} <div class="tab-panel">
onScroll={requestRecalculateTabIndicator} <div>
> <TimelinePanel
<div class="tab-panel"> client={client()}
<div> name="home"
<TimelinePanel prefetch={prefetching()}
client={client()} openFullScreenToot={openFullScreenToot}
name="home" />
prefetch={prefetching()} </div>
/>
</div> </div>
</div> <div class="tab-panel">
<div class="tab-panel"> <div>
<div> <TrendTimelinePanel
<TrendTimelinePanel client={client()} /> client={client()}
openFullScreenToot={openFullScreenToot}
/>
</div>
</div> </div>
</div> <div class="tab-panel">
<div class="tab-panel"> <div>
<div> <TimelinePanel
<TimelinePanel client={client()}
client={client()} name="public"
name="public" prefetch={prefetching()}
prefetch={prefetching()} openFullScreenToot={openFullScreenToot}
/> />
</div>
</div> </div>
<div></div>
</div> </div>
<div></div> </Show>
</div> </TimeSourceProvider>
</Show> <Suspense>
</TimeSourceProvider> <BottomSheet open={!!child()} onClose={() => pop(1)}>
{child()}
</BottomSheet>
</Suspense>
</HeroSourceProvider>
</Scaffold> </Scaffold>
</> </>
); );

View file

@ -10,7 +10,6 @@ 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;
@ -34,7 +33,6 @@ const PullDownToRefresh: Component<{
}); });
const rootVisible = obvx(() => rootElement); const rootVisible = obvx(() => rootElement);
const isFrameSuspended = useMaybeIsFrameSuspended()
createEffect(() => { createEffect(() => {
if (!rootVisible()) setPullDown(0); if (!rootVisible()) setPullDown(0);
@ -111,9 +109,6 @@ 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);
@ -164,9 +159,6 @@ 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,6 +20,12 @@ 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,6 +13,12 @@ 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(