From 8d9e4b1c484c41fc59d1a1681bafe650a22805df Mon Sep 17 00:00:00 2001 From: thislight Date: Thu, 24 Oct 2024 16:00:19 +0800 Subject: [PATCH 1/5] vite-plugin-pwa: enable dev sw for not production --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 7881869..5fdb81c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -64,7 +64,7 @@ export default defineConfig(({ mode }) => ({ strategies: "injectManifest", registerType: "autoUpdate", devOptions: { - enabled: mode === "staging" || mode === "dev", + enabled: mode !== "production", }, srcDir: "src/serviceworker", filename: "main.ts", From 3bc25786ca43e2c46475f70e83f0d5b5069960c4 Mon Sep 17 00:00:00 2001 From: thislight Date: Thu, 24 Oct 2024 22:20:38 +0800 Subject: [PATCH 2/5] masto/timelines: now the params is general typed --- src/masto/timelines.ts | 61 ++++++++++++++------------------- src/profiles/Profile.tsx | 4 +-- src/timelines/TimelinePanel.tsx | 2 +- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/masto/timelines.ts b/src/masto/timelines.ts index 8eb9c81..1483694 100644 --- a/src/masto/timelines.ts +++ b/src/masto/timelines.ts @@ -11,23 +11,15 @@ import { } from "solid-js"; import { createStore } from "solid-js/store"; -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 Timeline = { + list(params?: T): mastodon.Paginator; }; -export function createTimelineSnapshot( - timeline: Accessor, - limit: Accessor, -) { +type TimelineParamsOf = T extends Timeline ? P : never; + +export function createTimelineSnapshot< + T extends Timeline, +>(timeline: Accessor, limit: Accessor) { const [shot, { refetch }] = createResource( () => [timeline(), limit()] as const, async ([tl, limit]) => { @@ -80,13 +72,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; - limit: number; + params: T; }; type TreeNode = { @@ -108,21 +100,21 @@ function collectPath(node: TreeNode) { return path; } -function createTimelineChunk( - timeline: Accessor, - limit: Accessor, +function createTimelineChunk>( + timeline: Accessor, + params: Accessor>, ) { let vpMaxId: string | undefined, vpMinId: string | undefined; const fetchExtendingPage = async ( - tl: Timeline, + tl: T, direction: TimelineFetchDirection, - limit: number, + params: TimelineParamsOf, ) => { switch (direction) { case "next": { const page = await tl - .list({ limit, sinceId: vpMaxId }) + .list({ ...params, sinceId: vpMaxId }) .setDirection(direction) .next(); if ((page.value?.length ?? 0) > 0) { @@ -133,7 +125,7 @@ function createTimelineChunk( case "prev": { const page = await tl - .list({ limit, maxId: vpMinId }) + .list({ ...params, maxId: vpMinId }) .setDirection(direction) .next(); if ((page.value?.length ?? 0) > 0) { @@ -145,11 +137,11 @@ function createTimelineChunk( }; return createResource( - () => [timeline(), limit()] as const, + () => [timeline(), params()] as const, async ( - [tl, limit], + [tl, params], info: ResourceFetcherInfo< - Readonly, + Readonly>>, TimelineFetchDirection >, ) => { @@ -160,27 +152,26 @@ function createTimelineChunk( vpMaxId = undefined; vpMinId = undefined; } - const posts = await fetchExtendingPage(tl, direction, limit); + const posts = await fetchExtendingPage(tl, direction, params); return { tl, rebuilt: rebuildTimeline, chunk: posts.value ?? [], done: posts.done, direction, - limit, + params, }; }, ); } -export function createTimeline( - timeline: Accessor, - limit: Accessor, -) { +export function createTimeline< + T extends Timeline, +>(timeline: Accessor, params: Accessor>) { const lookup = new ReactiveMap>(); const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); - const [chunk, { refetch }] = createTimelineChunk(timeline, limit); + const [chunk, { refetch }] = createTimelineChunk(timeline, params); createEffect(() => { const chk = catchError(chunk, (e) => console.error(e)); diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx index cdea9d5..2230f3d 100644 --- a/src/profiles/Profile.tsx +++ b/src/profiles/Profile.tsx @@ -60,14 +60,14 @@ const Profile: Component = () => { const [recentToots] = createTimeline( () => session().client.v1.accounts.$select(params.id).statuses, - () => 20, + () => ({ limit: 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` diff --git a/src/timelines/TimelinePanel.tsx b/src/timelines/TimelinePanel.tsx index b1a014d..5c3e150 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], - () => 20, + () => ({limit: 20}), ); const [expandedThreadId, setExpandedThreadId] = createSignal(); const [typing, setTyping] = createSignal(false); From dff6abd379975e66b22303d0426b759ce24861b3 Mon Sep 17 00:00:00 2001 From: thislight Date: Thu, 24 Oct 2024 22:21:37 +0800 Subject: [PATCH 3/5] material: rename mui to theme --- src/App.tsx | 2 +- src/material/{mui.ts => theme.ts} | 0 src/platform/share.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/material/{mui.ts => theme.ts} (100%) diff --git a/src/App.tsx b/src/App.tsx index 028a234..875f408 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ import { lazy, onCleanup, } from "solid-js"; -import { useRootTheme } from "./material/mui.js"; +import { useRootTheme } from "./material/theme.js"; import { Provider as ClientProvider, createMastoClientFor, diff --git a/src/material/mui.ts b/src/material/theme.ts similarity index 100% rename from src/material/mui.ts rename to src/material/theme.ts diff --git a/src/platform/share.tsx b/src/platform/share.tsx index 5f80dc0..7c98cb9 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/mui"; +import { useRootTheme } from "../material/theme"; const ShareBottomSheet: Component<{ data?: ShareData; From b22ac67c3aa08bf851adc8ca6d9d13ae087f0234 Mon Sep 17 00:00:00 2001 From: thislight Date: Thu, 24 Oct 2024 23:47:44 +0800 Subject: [PATCH 4/5] Profile: filter recent toots - material: new Menu component - material/theme: animation curves - TootFilterButton --- src/App.tsx | 2 +- src/material/Menu.tsx | 136 ++++++++++++++++++++++++++++++ src/material/theme.ts | 8 ++ src/profiles/Profile.tsx | 31 ++++++- src/profiles/TootFilterButton.tsx | 112 ++++++++++++++++++++++++ 5 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 src/material/Menu.tsx create mode 100644 src/profiles/TootFilterButton.tsx diff --git a/src/App.tsx b/src/App.tsx index 875f408..abdff31 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -149,7 +149,7 @@ const App: Component = () => { return ; }} > - + ; + 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/theme.ts index 6527cc4..a7c2c65 100644 --- a/src/material/theme.ts +++ b/src/material/theme.ts @@ -2,6 +2,9 @@ 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({ @@ -15,3 +18,8 @@ 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/profiles/Profile.tsx b/src/profiles/Profile.tsx index 2230f3d..64cf0b5 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,7 +48,6 @@ const Profile: Component = () => { threshold: 0.1, }, ); - onCleanup(() => obx.disconnect()); const [profile] = createResource( @@ -58,9 +57,18 @@ const Profile: Component = () => { }, ); + const [recentTootFilter, setRecentTootFilter] = createSignal({ + boost: true, + reply: true, + original: true, + }); + const [recentToots] = createTimeline( () => session().client.v1.accounts.$select(params.id).statuses, - () => ({ limit: 20 }), + () => { + const { boost, reply } = recentTootFilter(); + return { limit: 20, excludeReblogs: !boost, excludeReplies: !reply }; + }, ); const bannerImg = () => profile()?.header; @@ -126,7 +134,9 @@ const Profile: Component = () => { variant="dense" sx={{ display: "flex", - color: scrolledPastBanner() ? undefined : bannerSampledColors()?.text, + color: scrolledPastBanner() + ? undefined + : bannerSampledColors()?.text, paddingTop: "var(--safe-area-inset-top)", }} > @@ -238,6 +248,19 @@ 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; From 4075c4194291e9126528cb22b9625cca42af882d Mon Sep 17 00:00:00 2001 From: thislight Date: Thu, 24 Oct 2024 23:50:57 +0800 Subject: [PATCH 5/5] vite-plugin-pwa: enable production sw for staging --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 5fdb81c..466d139 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -64,7 +64,7 @@ export default defineConfig(({ mode }) => ({ strategies: "injectManifest", registerType: "autoUpdate", devOptions: { - enabled: mode !== "production", + enabled: !["production", "staging"].includes(mode), }, srcDir: "src/serviceworker", filename: "main.ts",