tutu/src/timelines/Home.tsx
2025-01-02 21:05:05 +08:00

255 lines
7.2 KiB
TypeScript

import {
createSignal,
Show,
type ParentComponent,
createEffect,
useTransition,
} from "solid-js";
import { useDocumentTitle } from "../utils";
import Scaffold from "~material/Scaffold";
import {
ListItemSecondaryAction,
ListItemText,
MenuItem,
Switch,
} from "@suid/material";
import { css } from "solid-styled";
import { TimeSourceProvider, createTimeSource } from "~platform/timesrc";
import ProfileMenuButton from "./ProfileMenuButton";
import Tabs from "~material/Tabs";
import Tab from "~material/Tab";
import { $settings } from "../settings/stores";
import { useStore } from "@nanostores/solid";
import TrendTimelinePanel from "./TrendTimelinePanel";
import TimelinePanel from "./TimelinePanel";
import { useSessions } from "../masto/clients";
import {
createSingluarItemSelection,
default as ItemSelectionProvider,
} from "./toots/ItemSelectionProvider";
import AppTopBar from "~material/AppTopBar";
import { createTranslator } from "~platform/i18n";
import { useWindowSize } from "@solid-primitives/resize-observer";
type StringRes = Record<
"tabs.home" | "tabs.trending" | "tabs.public" | "set.prefetch-toots",
string
>;
const Home: ParentComponent = (props) => {
let panelList: HTMLDivElement;
useDocumentTitle("Timelines");
const [t] = createTranslator(
(code) => import(`./i18n/${code}.json`) as Promise<{ default: StringRes }>,
);
const now = createTimeSource();
const [, selectionState] = createSingluarItemSelection(
undefined as string | undefined,
);
const settings$ = useStore($settings);
const profiles = useSessions();
const client = () => {
const all = profiles();
return all?.[0]?.client;
};
const prefetching = () => !settings$().prefetchTootsDisabled;
const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [
number,
number,
]);
let scrollEventLockReleased = true;
const recalculateTabIndicator = () => {
scrollEventLockReleased = false;
try {
if (!panelList!) return;
const { x: panelX, width: panelWidth } =
panelList.getBoundingClientRect();
let minIdx = +Infinity,
maxIdx = -Infinity;
const items = panelList.querySelectorAll(".tab-panel");
const ranges = Array.from(items).map((x) => {
const rect = x.getBoundingClientRect();
const inlineStart = rect.x - panelX;
const inlineEnd = rect.width + inlineStart;
return [inlineStart, inlineEnd] as const;
});
for (let i = 0; i < items.length; i++) {
const e = items.item(i);
const [inlineStart, inlineEnd] = ranges[i];
if (inlineStart >= 0 && inlineEnd <= panelWidth) {
minIdx = Math.min(minIdx, i);
maxIdx = Math.max(maxIdx, i);
e.classList.add("active");
} else {
e.classList.remove("active");
}
}
if (isFinite(minIdx) && isFinite(maxIdx)) {
setFocusRange([minIdx, maxIdx]);
}
} finally {
scrollEventLockReleased = true;
}
};
const requestRecalculateTabIndicator = () => {
if (scrollEventLockReleased) {
requestAnimationFrame(recalculateTabIndicator);
}
};
const windowSize = useWindowSize();
createEffect((last) => {
if (last !== windowSize.width) {
requestRecalculateTabIndicator();
}
});
const [inTransition] = useTransition();
createEffect(() => {
if (!inTransition()) {
requestAnimationFrame(recalculateTabIndicator);
}
});
const isTabFocus = (idx: number) => {
const [start, end] = focusRange();
if (!isFinite(start) || !isFinite(end)) return false;
return idx >= start && idx <= end;
};
const onTabClick = (idx: number) => {
const items = panelList!.querySelectorAll(".tab-panel");
if (items.length > idx) {
items.item(idx).scrollIntoView({ block: "start", behavior: "smooth" });
}
if (isTabFocus(idx)) {
items.item(idx).scrollTo({
top: 0,
behavior: "smooth",
});
}
};
css`
.tab-panel {
overflow: visible auto;
max-width: 560px;
height: 100%;
padding: 0 16px;
scroll-snap-align: center;
overscroll-behavior-block: none;
contain: strict;
contain-intrinsic-size: auto 560px auto 100vh;
contain-intrinsic-size: auto 560px auto 100dvh;
@media (max-width: 600px) {
padding: 0;
}
}
.panel-list {
display: grid;
grid-auto-columns: 560px;
grid-auto-flow: column;
overflow: auto hidden;
scroll-snap-type: x mandatory;
scroll-snap-stop: always;
height: calc(100vh - var(--scaffold-topbar-height, 0px));
height: calc(100dvh - var(--scaffold-topbar-height, 0px));
padding-left: var(--safe-area-inset-left, 0);
padding-right: var(--safe-area-inset-right, 0);
& > * {
content-visibility: auto;
}
@media (max-width: 600px) {
grid-auto-columns: 100%;
}
}
`;
return (
<>
<Scaffold
topbar={
<AppTopBar>
<Tabs>
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
{t("tabs.home")}
</Tab>
<Tab focus={isTabFocus(1)} onClick={[onTabClick, 1]}>
{t("tabs.trending")}
</Tab>
<Tab focus={isTabFocus(2)} onClick={[onTabClick, 2]}>
{t("tabs.public")}
</Tab>
</Tabs>
<ProfileMenuButton profile={profiles()[0]}>
<MenuItem
onClick={(e) =>
$settings.setKey(
"prefetchTootsDisabled",
!$settings.get().prefetchTootsDisabled,
)
}
>
<ListItemText>{t("set.prefetch-toots")}</ListItemText>
<ListItemSecondaryAction>
<Switch checked={prefetching()}></Switch>
</ListItemSecondaryAction>
</MenuItem>
</ProfileMenuButton>
</AppTopBar>
}
>
<ItemSelectionProvider value={selectionState}>
<TimeSourceProvider value={now}>
<Show when={!!client()}>
<div
class="panel-list"
ref={panelList!}
onScroll={requestRecalculateTabIndicator}
>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="home"
prefetch={prefetching()}
/>
</div>
</div>
<div class="tab-panel">
<div>
<TrendTimelinePanel client={client()} />
</div>
</div>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="public"
prefetch={prefetching()}
/>
</div>
</div>
<div></div>
</div>
</Show>
</TimeSourceProvider>
</ItemSelectionProvider>
</Scaffold>
</>
);
};
export default Home;