289 lines
8.4 KiB
TypeScript
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;
|