tutu/src/timelines/Home.tsx

284 lines
8.2 KiB
TypeScript
Raw Normal View History

2024-07-14 20:28:44 +08:00
import {
createSignal,
Show,
onMount,
2024-07-22 21:57:04 +08:00
type ParentComponent,
children,
2024-08-12 21:55:26 +08:00
Suspense,
2024-07-14 20:28:44 +08:00
} 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";
2024-07-23 08:28:47 +08:00
import { $settings } from "../settings/stores";
import { useStore } from "@nanostores/solid";
import { HeroSourceProvider, type HeroSource } from "../platform/anim";
import { useNavigate } from "@solidjs/router";
2024-08-12 21:55:26 +08:00
import { useSignedInProfiles } from "../masto/acct";
2024-08-13 14:17:33 +08:00
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
import TrendTimelinePanel from "./TrendTimelinePanel";
import TimelinePanel from "./TimelinePanel";
2024-07-14 20:28:44 +08:00
2024-07-22 21:57:04 +08:00
const Home: ParentComponent = (props) => {
2024-07-14 20:28:44 +08:00
let panelList: HTMLDivElement;
useDocumentTitle("Timelines");
const now = createTimeSource();
2024-08-05 15:33:00 +08:00
const settings$ = useStore($settings);
2024-08-12 21:55:26 +08:00
const [profiles] = useSignedInProfiles();
const profile = () => {
const all = profiles();
if (all.length > 0) {
return all[0].inf;
}
};
const client = () => {
const all = profiles();
return all?.[0]?.client;
};
const navigate = useNavigate();
2024-07-14 20:28:44 +08:00
const [heroSrc, setHeroSrc] = createSignal<HeroSource>({});
2024-07-14 20:28:44 +08:00
const [panelOffset, setPanelOffset] = createSignal(0);
2024-08-05 15:33:00 +08:00
const prefetching = () => !settings$().prefetchTootsDisabled;
2024-07-14 20:28:44 +08:00
const [currentFocusOn, setCurrentFocusOn] = createSignal<HTMLElement[]>([]);
const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [
number,
number,
]);
2024-08-05 15:33:00 +08:00
const child = children(() => props.children);
2024-07-22 21:57:04 +08:00
2024-07-14 20:28:44 +08:00
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" });
2024-07-14 20:28:44 +08:00
}
if (isTabFocus(idx)) {
items.item(idx).scrollTo({
top: 0,
behavior: "smooth",
});
}
2024-07-14 20:28:44 +08:00
};
const openFullScreenToot = (
toot: mastodon.v1.Status,
srcElement?: HTMLElement,
2024-09-28 15:29:21 +08:00
reply?: boolean,
) => {
2024-08-12 21:55:26 +08:00
const p = profiles()[0];
const inf = p.account.inf ?? profile();
if (!inf) {
2024-08-12 21:55:26 +08:00
console.warn("no account info?");
return;
}
setHeroSrc((x) =>
Object.assign({}, x, { [BOTTOM_SHEET_HERO]: srcElement }),
);
const acct = `${inf.username}@${p.account.site}`;
2024-08-13 14:17:33 +08:00
setTootBottomSheetCache(acct, toot);
navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
2024-09-28 15:29:21 +08:00
state: reply
? {
tootReply: true,
}
: undefined,
});
};
2024-07-14 20:28:44 +08:00
css`
.tab-panel {
overflow: visible auto;
max-width: 560px;
height: 100%;
2024-08-07 17:04:54 +08:00
padding: 0 16px;
2024-07-14 20:28:44 +08:00
scroll-snap-align: center;
2024-08-07 17:04:54 +08:00
overscroll-behavior-block: none;
2024-07-14 20:28:44 +08:00
@media (max-width: 600px) {
padding: 0;
}
}
.panel-list {
display: grid;
grid-auto-columns: 560px;
grid-auto-flow: column;
overflow-x: auto;
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));
2024-07-24 14:27:20 +08:00
padding-left: var(--safe-area-inset-left, 0);
padding-right: var(--safe-area-inset-right, 0);
2024-07-14 20:28:44 +08:00
@media (max-width: 600px) {
grid-auto-columns: 100%;
}
}
`;
return (
2024-07-22 21:57:04 +08:00
<>
<Scaffold
topbar={
<AppBar position="static">
2024-08-05 15:33:00 +08:00
<Toolbar
variant="dense"
class="responsive"
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
>
2024-07-22 21:57:04 +08:00
<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={profile()}>
2024-08-05 15:33:00 +08:00
<MenuItem
onClick={(e) =>
$settings.setKey(
"prefetchTootsDisabled",
!$settings.get().prefetchTootsDisabled,
)
}
>
2024-07-22 21:57:04 +08:00
<ListItemText>Prefetch Toots</ListItemText>
<ListItemSecondaryAction>
<Switch checked={prefetching()}></Switch>
</ListItemSecondaryAction>
</MenuItem>
</ProfileMenuButton>
</Toolbar>
</AppBar>
}
>
<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>
2024-07-22 21:57:04 +08:00
</div>
<div class="tab-panel">
<div>
<TrendTimelinePanel
client={client()}
prefetch={prefetching()}
openFullScreenToot={openFullScreenToot}
/>
</div>
2024-07-22 21:57:04 +08:00
</div>
<div class="tab-panel">
<div>
<TimelinePanel
client={client()}
name="public"
prefetch={prefetching()}
openFullScreenToot={openFullScreenToot}
/>
</div>
2024-07-22 21:57:04 +08:00
</div>
<div></div>
2024-07-14 20:28:44 +08:00
</div>
</Show>
2024-07-22 21:57:04 +08:00
</TimeSourceProvider>
2024-08-12 21:55:26 +08:00
<Suspense>
<HeroSourceProvider value={[heroSrc, setHeroSrc]}>
2024-10-12 19:48:34 +08:00
<BottomSheet open={!!child()} onClose={() => navigate(-1)}>
{child()}
</BottomSheet>
2024-08-12 21:55:26 +08:00
</HeroSourceProvider>
</Suspense>
2024-07-22 21:57:04 +08:00
</Scaffold>
</>
2024-07-14 20:28:44 +08:00
);
};
export default Home;