tutu/src/timelines/Home.tsx
thislight dde0c249f4
useSignedInProfiles: removed
- use useSessions() instead
2024-11-04 17:09:20 +08:00

289 lines
8.4 KiB
TypeScript

import {
createSignal,
Show,
onMount,
type ParentComponent,
children,
Suspense,
} from "solid-js";
import { useDocumentTitle } from "../utils";
import { type mastodon } from "masto";
import Scaffold from "../material/Scaffold";
import {
AppBar,
ListItemSecondaryAction,
ListItemText,
MenuItem,
Switch,
Toolbar,
} 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 { 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 { useNavigate } from "@solidjs/router";
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
import TrendTimelinePanel from "./TrendTimelinePanel";
import TimelinePanel from "./TimelinePanel";
import { useSessions } from "../masto/clients";
const Home: ParentComponent = (props) => {
let panelList: HTMLDivElement;
useDocumentTitle("Timelines");
const now = createTimeSource();
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 navigate = useNavigate();
const [heroSrc, setHeroSrc] = createSignal<HeroSource>({});
const [panelOffset, setPanelOffset] = createSignal(0);
const prefetching = () => !settings$().prefetchTootsDisabled;
const [currentFocusOn, setCurrentFocusOn] = createSignal<HTMLElement[]>([]);
const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [
number,
number,
]);
const child = children(() => props.children);
let scrollEventLockReleased = true;
const recalculateTabIndicator = () => {
scrollEventLockReleased = false;
try {
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;
}
};
onMount(() => {
makeEventListener(panelList, "scroll", () => {
if (scrollEventLockReleased) {
requestAnimationFrame(recalculateTabIndicator);
}
});
makeEventListener(window, "resize", () => {
if (scrollEventLockReleased) {
requestAnimationFrame(recalculateTabIndicator);
}
});
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",
});
}
};
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);
navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
state: reply
? {
tootReply: true,
}
: undefined,
});
};
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={
<AppBar position="static">
<Toolbar
variant="dense"
class="responsive"
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
>
<Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
Home
</Tab>
<Tab focus={isTabFocus(1)} onClick={[onTabClick, 1]}>
Trending
</Tab>
<Tab focus={isTabFocus(2)} onClick={[onTabClick, 2]}>
Public
</Tab>
</Tabs>
<ProfileMenuButton profile={profiles()[0]}>
<MenuItem
onClick={(e) =>
$settings.setKey(
"prefetchTootsDisabled",
!$settings.get().prefetchTootsDisabled,
)
}
>
<ListItemText>Prefetch Toots</ListItemText>
<ListItemSecondaryAction>
<Switch checked={prefetching()}></Switch>
</ListItemSecondaryAction>
</MenuItem>
</ProfileMenuButton>
</Toolbar>
</AppBar>
}
>
<HeroSourceProvider value={[heroSrc, setHeroSrc]}>
<TimeSourceProvider value={now}>
<Show when={!!client()}>
<div class="panel-list" ref={panelList!}>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="home"
prefetch={prefetching()}
openFullScreenToot={openFullScreenToot}
/>
</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>
</Show>
</TimeSourceProvider>
<Suspense>
<BottomSheet open={!!child()} onClose={() => navigate(-1)}>
{child()}
</BottomSheet>
</Suspense>
</HeroSourceProvider>
</Scaffold>
</>
);
};
export default Home;