diff --git a/src/App.tsx b/src/App.tsx index abdff31..028a234 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ import { lazy, onCleanup, } from "solid-js"; -import { useRootTheme } from "./material/theme.js"; +import { useRootTheme } from "./material/mui.js"; import { Provider as ClientProvider, createMastoClientFor, @@ -149,7 +149,7 @@ const App: Component = () => { return ; }} > - + = { - list(params?: T): mastodon.Paginator; +type Timeline = { + list(params: { + /** Return results older than this ID. */ + readonly maxId?: string; + /** Return results newer than this ID. */ + readonly sinceId?: string; + /** Get a list of items with ID greater than this value excluding this ID */ + readonly minId?: string; + /** Maximum number of results to return per page. Defaults to 40. NOTE: Pagination is done with the Link header from the response. */ + readonly limit?: number; + }): mastodon.Paginator; }; -type TimelineParamsOf = T extends Timeline ? P : never; - -export function createTimelineSnapshot< - T extends Timeline, ->(timeline: Accessor, limit: Accessor) { +export function createTimelineSnapshot( + timeline: Accessor, + limit: Accessor, +) { const [shot, { refetch }] = createResource( () => [timeline(), limit()] as const, async ([tl, limit]) => { @@ -72,13 +80,13 @@ export function createTimelineSnapshot< export type TimelineFetchDirection = mastodon.Direction; -export type TimelineChunk = { - tl: Timeline; +export type TimelineChunk = { + tl: Timeline; rebuilt: boolean; chunk: readonly mastodon.v1.Status[]; done?: boolean; direction: TimelineFetchDirection; - params: T; + limit: number; }; type TreeNode = { @@ -100,21 +108,21 @@ function collectPath(node: TreeNode) { return path; } -function createTimelineChunk>( - timeline: Accessor, - params: Accessor>, +function createTimelineChunk( + timeline: Accessor, + limit: Accessor, ) { let vpMaxId: string | undefined, vpMinId: string | undefined; const fetchExtendingPage = async ( - tl: T, + tl: Timeline, direction: TimelineFetchDirection, - params: TimelineParamsOf, + limit: number, ) => { switch (direction) { case "next": { const page = await tl - .list({ ...params, sinceId: vpMaxId }) + .list({ limit, sinceId: vpMaxId }) .setDirection(direction) .next(); if ((page.value?.length ?? 0) > 0) { @@ -125,7 +133,7 @@ function createTimelineChunk 0) { @@ -137,11 +145,11 @@ function createTimelineChunk [timeline(), params()] as const, + () => [timeline(), limit()] as const, async ( - [tl, params], + [tl, limit], info: ResourceFetcherInfo< - Readonly>>, + Readonly, TimelineFetchDirection >, ) => { @@ -152,26 +160,27 @@ function createTimelineChunk, ->(timeline: Accessor, params: Accessor>) { +export function createTimeline( + timeline: Accessor, + limit: Accessor, +) { const lookup = new ReactiveMap>(); const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); - const [chunk, { refetch }] = createTimelineChunk(timeline, params); + const [chunk, { refetch }] = createTimelineChunk(timeline, limit); createEffect(() => { const chk = catchError(chunk, (e) => console.error(e)); diff --git a/src/material/Menu.tsx b/src/material/Menu.tsx deleted file mode 100644 index 365d090..0000000 --- a/src/material/Menu.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useWindowSize } from "@solid-primitives/resize-observer"; -import { MenuList } from "@suid/material"; -import { - createEffect, - createSignal, - type JSX, - type ParentComponent, -} from "solid-js"; -import { ANIM_CURVE_STD } from "./theme"; - -type Props = { - open?: boolean; - onClose?: JSX.EventHandlerUnion; - anchor: () => DOMRect; -}; - -function adjustMenuPosition( - rect: DOMRect, - [left, top]: [number, number], - { width, height }: { width: number; height: number }, -) { - const ntop = rect.bottom > height ? top - (rect.bottom - height) : top; - const nleft = rect.right > width ? left - (rect.right - width) : left; - return [nleft, ntop] as [number, number]; -} - -const Menu: ParentComponent = (props) => { - let root: HTMLDialogElement; - const [pos, setPos] = createSignal<[number, number]>([0, 0]); - const windowSize = useWindowSize(); - - createEffect(() => { - if (props.open) { - const a = props.anchor(); - if (!root.open) { - root.showModal(); - const rend = root.getBoundingClientRect(); - - setPos(adjustMenuPosition(rend, [a.left, a.top], windowSize)); - - const overflow = root.style.overflow; - root.style.overflow = "hidden"; - const duration = (rend.height / 1600) * 1000; - const easing = ANIM_CURVE_STD; - const animation = root.animate( - { - height: [`${rend.height / 2}px`, `${rend.height}px`], - width: [`${rend.width / 4 * 3}px`, `${rend.width}px`], - }, - { - duration, - easing, - }, - ); - animation.addEventListener( - "finish", - () => (root.style.overflow = overflow), - ); - } else { - setPos( - adjustMenuPosition( - root.getBoundingClientRect(), - [a.left, a.top], - windowSize, - ), - ); - } - } else { - animateClose(); - } - }); - - const animateClose = () => { - const rend = root.getBoundingClientRect(); - const overflow = root.style.overflow; - root.style.overflow = "hidden"; - const animation = root.animate( - { - height: [`${rend.height}px`, `${rend.height / 2}px`], - width: [`${rend.width}px`, `${rend.width / 4 * 3}px`], - }, - { - duration: (rend.height / 2 / 1600) * 1000, - easing: ANIM_CURVE_STD, - }, - ); - animation.addEventListener("finish", () => { - root.style.overflow = overflow; - root.close(); - }); - }; - - return ( - { - if (e.target === root) { - if (props.onClose) { - if (Array.isArray(props.onClose)) { - props.onClose[0](props.onClose[1], e); - } else { - ( - props.onClose as ( - event: Event & { currentTarget: HTMLDialogElement }, - ) => void - )(e); - } - } - e.stopPropagation(); - } - }} - style={{ - position: "absolute", - left: `${pos()[0]}px`, - top: `${pos()[1]}px`, - border: "none", - padding: 0, - "max-width": "560px", - width: "max-content", - /*"min-width": "20vw", */ - "box-shadow": "var(--tutu-shadow-e8)", - }} - > -
- {props.children} -
-
- ); -}; - -export default Menu; diff --git a/src/material/theme.ts b/src/material/mui.ts similarity index 57% rename from src/material/theme.ts rename to src/material/mui.ts index a7c2c65..6527cc4 100644 --- a/src/material/theme.ts +++ b/src/material/mui.ts @@ -2,9 +2,6 @@ import { Theme, createTheme } from "@suid/material/styles"; import { deepPurple, amber } from "@suid/material/colors"; import { Accessor } from "solid-js"; -/** - * The MUI theme. - */ export function useRootTheme(): Accessor { return () => createTheme({ @@ -18,8 +15,3 @@ export function useRootTheme(): Accessor { }, }); } - -export const ANIM_CURVE_STD = "cubic-bezier(0.4, 0, 0.2, 1)"; -export const ANIM_CURVE_DECELERATION = "cubic-bezier(0, 0, 0.2, 1)"; -export const ANIM_CURVE_ACELERATION = "cubic-bezier(0.4, 0, 1, 1)"; -export const ANIM_CURVE_SHARP = "cubic-bezier(0.4, 0, 0.6, 1)"; diff --git a/src/platform/share.tsx b/src/platform/share.tsx index 7c98cb9..5f80dc0 100644 --- a/src/platform/share.tsx +++ b/src/platform/share.tsx @@ -16,7 +16,7 @@ import { import { Close as CloseIcon, ContentCopy } from "@suid/icons-material"; import { Title } from "../material/typography"; import { render } from "solid-js/web"; -import { useRootTheme } from "../material/theme"; +import { useRootTheme } from "../material/mui"; const ShareBottomSheet: Component<{ data?: ShareData; diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx index 64cf0b5..cdea9d5 100644 --- a/src/profiles/Profile.tsx +++ b/src/profiles/Profile.tsx @@ -19,8 +19,8 @@ import { useWindowSize } from "@solid-primitives/resize-observer"; import { css } from "solid-styled"; import { createTimeline } from "../masto/timelines"; import TootList from "../timelines/TootList"; +import { createIntersectionObserver } from "@solid-primitives/intersection-observer"; import { createTimeSource, TimeSourceProvider } from "../platform/timesrc"; -import TootFilterButton from "./TootFilterButton"; const Profile: Component = () => { const navigate = useNavigate(); @@ -48,6 +48,7 @@ const Profile: Component = () => { threshold: 0.1, }, ); + onCleanup(() => obx.disconnect()); const [profile] = createResource( @@ -57,25 +58,16 @@ const Profile: Component = () => { }, ); - const [recentTootFilter, setRecentTootFilter] = createSignal({ - boost: true, - reply: true, - original: true, - }); - const [recentToots] = createTimeline( () => session().client.v1.accounts.$select(params.id).statuses, - () => { - const { boost, reply } = recentTootFilter(); - return { limit: 20, excludeReblogs: !boost, excludeReplies: !reply }; - }, + () => 20, ); const bannerImg = () => profile()?.header; const avatarImg = () => profile()?.avatar; const displayName = () => resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []); - const fullUsername = () => `@${profile()?.acct ?? ""}`; // TODO: full user name + const fullUsername = () => `@${profile()?.acct ?? "..."}`; // TODO: full user name const description = () => profile()?.note; css` @@ -134,9 +126,7 @@ const Profile: Component = () => { variant="dense" sx={{ display: "flex", - color: scrolledPastBanner() - ? undefined - : bannerSampledColors()?.text, + color: scrolledPastBanner() ? undefined : bannerSampledColors()?.text, paddingTop: "var(--safe-area-inset-top)", }} > @@ -248,19 +238,6 @@ const Profile: Component = () => { -
- -
- > = { - options: Filters; - applied: Record; - disabledKeys?: (keyof Filters)[]; - - onApply(value: Record): void; -}; - -function TootFilterButton>(props: Props) { - const buttonId = createUniqueId(); - const [open, setOpen] = createSignal(false); - - const getTextForMultipleEntities = (texts: string[]) => { - switch (texts.length) { - case 0: - return "Nothing"; - case 1: - return texts[0]; - case 2: - return `${texts[0]} and ${texts[1]}`; - case 3: - return `${texts[0]}, ${texts[1]} and ${texts[2]}`; - default: - return `${texts[0]} and ${texts.length - 1} other${texts.length > 2 ? "s" : ""}`; - } - }; - - const optionKeys = () => Object.keys(props.options); - - const appliedKeys = createMemo(() => { - const applied = props.applied; - return optionKeys().filter((k) => applied[k]); - }); - - const text = () => { - const keys = optionKeys(); - const napplied = appliedKeys().length; - switch (napplied) { - case keys.length: - return "All"; - default: - return getTextForMultipleEntities( - appliedKeys().map((k) => props.options[k]), - ); - } - }; - - const toggleKey = (key: keyof F) => { - props.onApply( - Object.assign({}, props.applied, { - [key]: !props.applied[key], - }), - ); - }; - - return ( - <> - - - document.getElementById(buttonId)!.getBoundingClientRect() - } - > - - {(item, idx) => ( - <> - - {props.options[item]} - - - - )} - - - - ); -} - -export default TootFilterButton; diff --git a/src/timelines/TimelinePanel.tsx b/src/timelines/TimelinePanel.tsx index 5c3e150..b1a014d 100644 --- a/src/timelines/TimelinePanel.tsx +++ b/src/timelines/TimelinePanel.tsx @@ -32,7 +32,7 @@ const TimelinePanel: Component<{ const [timeline, snapshot, { refetch: refetchTimeline }] = createTimeline( () => props.client.v1.timelines[props.name], - () => ({limit: 20}), + () => 20, ); const [expandedThreadId, setExpandedThreadId] = createSignal(); const [typing, setTyping] = createSignal(false); diff --git a/vite.config.ts b/vite.config.ts index 466d139..7881869 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -64,7 +64,7 @@ export default defineConfig(({ mode }) => ({ strategies: "injectManifest", registerType: "autoUpdate", devOptions: { - enabled: !["production", "staging"].includes(mode), + enabled: mode === "staging" || mode === "dev", }, srcDir: "src/serviceworker", filename: "main.ts",