import { Component, For, onCleanup, createSignal, createEffect, Show, untrack, onMount, } from "solid-js"; import { $accounts } from "../accounts/stores"; import { useDocumentTitle } from "../utils"; import { useStore } from "@nanostores/solid"; import { useMastoClientFor } from "../masto/clients"; import { type mastodon } from "masto"; import Scaffold from "../material/Scaffold"; import { AppBar, Button, Fab, LinearProgress, ListItemSecondaryAction, ListItemText, MenuItem, Switch, Toolbar, Typography, } from "@suid/material"; import { css } from "solid-styled"; import { TimeSourceProvider, createTimeSource } from "../platform/timesrc"; import TootThread from "./TootThread.js"; import { useAcctProfile } from "../masto/acct"; import ProfileMenuButton from "./ProfileMenuButton"; import Tabs from "../material/Tabs"; import Tab from "../material/Tab"; import { Create as CreateTootIcon } from "@suid/icons-material"; import { useTimeline } from "../masto/timelines"; import { makeEventListener } from "@solid-primitives/event-listener"; const TimelinePanel: Component<{ client: mastodon.rest.Client; name: "home" | "public" | "trends"; prefetch?: boolean; }> = (props) => { const [timeline, { refetch: refetchTimeline, mutate: mutateTimeline }] = useTimeline(() => props.name !== "trends" ? props.client.v1.timelines[props.name] : props.client.v1.trends.statuses, ); const tlEndObserver = new IntersectionObserver(() => { if (untrack(() => props.prefetch) && !timeline.loading) refetchTimeline({ direction: "old" }); }); onCleanup(() => tlEndObserver.disconnect()); const onBookmark = async ( index: number, client: mastodon.rest.Client, status: mastodon.v1.Status, ) => { const result = await (status.bookmarked ? client.v1.statuses.$select(status.id).unbookmark() : client.v1.statuses.$select(status.id).bookmark()); mutateTimeline((o) => { o[index] = result; return o; }); }; const onBoost = async ( index: number, client: mastodon.rest.Client, status: mastodon.v1.Status, ) => { const reblogged = false; mutateTimeline((o) => { Object.assign(o[index].reblog ?? o[index], { reblogged: !reblogged, }); return o; }); const result = reblogged ? await client.v1.statuses.$select(status.id).unreblog() : (await client.v1.statuses.$select(status.id).reblog()).reblog!; mutateTimeline((o) => { Object.assign(o[index].reblog ?? o[index], { reblogged: result.reblogged, reblogsCount: result.reblogsCount, }); return o; }); }; return ( <>
{(item, index) => { return ( onBoost(index(), ...args)} onBookmark={(...args) => onBookmark(index(), ...args)} client={props.client} /> ); }}
tlEndObserver.observe(e)}>
); }; const Home: Component = () => { let panelList: HTMLDivElement; useDocumentTitle("Timelines"); const accounts = useStore($accounts); const now = createTimeSource(); const client = useMastoClientFor(() => accounts()[0]); const [profile] = useAcctProfile(() => accounts()[0]); const [panelOffset, setPanelOffset] = createSignal(0); const [prefetching, setPrefetching] = createSignal(true); const [currentFocusOn, setCurrentFocusOn] = createSignal([]); const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [ number, number, ]); 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({ behavior: "smooth" }); } }; css` .tab-panel { overflow: visible auto; max-width: 560px; height: 100%; padding: 40px 16px; max-height: calc(100vh - var(--scaffold-topbar-height, 0px)); max-height: calc(100dvh - var(--scaffold-topbar-height, 0px)); scroll-snap-align: center; &:not(.active) { overflow: hidden; } @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)); @media (max-width: 600px) { grid-auto-columns: 100%; } } `; return ( Home Trending Public setPrefetching((x) => !x)}> Prefetch Toots } fab={ } >
); }; export default Home;