diff --git a/bun.lockb b/bun.lockb index 42d6188..72491e0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d4bfb92..3c10737 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@solid-primitives/i18n": "^2.1.1", "@solid-primitives/intersection-observer": "^2.1.6", "@solid-primitives/map": "^0.4.13", + "@solid-primitives/page-visibility": "^2.0.17", "@solid-primitives/resize-observer": "^2.0.26", "@solidjs/router": "^0.14.10", "@suid/icons-material": "^0.8.1", diff --git a/src/platform/timesrc.ts b/src/platform/timesrc.ts index d5d957a..12daee0 100644 --- a/src/platform/timesrc.ts +++ b/src/platform/timesrc.ts @@ -1,3 +1,4 @@ +import { usePageVisibility } from "@solid-primitives/page-visibility"; import { Accessor, createContext, @@ -15,19 +16,30 @@ export const TimeSourceProvider = TimeSourceContext.Provider; export function createTimeSource() { let id: ReturnType | undefined; const [get, set] = createSignal(new Date()); + const visible = usePageVisibility(); - createRenderEffect(() => - untrack(() => { - id = setTimeout(() => { - set(new Date()); - }, 30 * 1000); - }), - ); - - onCleanup(() => { + const cancelTimer = () => { if (typeof id !== "undefined") { clearInterval(id); } + id = undefined; + }; + + const resetTimer = () => { + cancelTimer(); + set(new Date()); + id = setTimeout(() => { + set(new Date()); + }, 30 * 1000); // refresh rate: 30s + }; + + createRenderEffect(() => { + onCleanup(cancelTimer); + if (visible()) { + resetTimer(); + } else { + console.debug("createTimeSource: page is invisible, cancel the timer") + } }); return get; diff --git a/src/profiles/Profile.tsx b/src/profiles/Profile.tsx index e557440..9024820 100644 --- a/src/profiles/Profile.tsx +++ b/src/profiles/Profile.tsx @@ -13,6 +13,7 @@ import { AppBar, Avatar, Button, + CircularProgress, Divider, IconButton, ListItemIcon, @@ -102,7 +103,7 @@ const Profile: Component = () => { const avatarImg = () => profile()?.avatar; const displayName = () => resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []); - const fullUsername = () => `@${profile()?.acct ?? ""}`; // TODO: full user name + const fullUsername = () => (profile()?.acct ? `@${profile()!.acct!}` : ""); // TODO: full user name const description = () => profile()?.note; css` @@ -231,7 +232,12 @@ const Profile: Component = () => { Mention {profile()?.displayName || ""}... - + @@ -307,6 +313,7 @@ const Profile: Component = () => { createRenderEffect(() => (e.innerHTML = description() || "")) } > + @@ -365,8 +372,11 @@ const Profile: Component = () => { size="large" color="primary" onClick={[refetchRecentToots, "prev"]} + disabled={recentTootChunk.loading} > - + }> + + diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index d229da8..268b846 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -249,7 +249,6 @@ const Home: ParentComponent = (props) => {
diff --git a/src/timelines/MediaAttachmentGrid.tsx b/src/timelines/MediaAttachmentGrid.tsx index 2ae56be..dcc5af1 100644 --- a/src/timelines/MediaAttachmentGrid.tsx +++ b/src/timelines/MediaAttachmentGrid.tsx @@ -2,29 +2,52 @@ import type { mastodon } from "masto"; import { type Component, For, - createEffect, + createMemo, createRenderEffect, createSignal, onCleanup, - onMount, } from "solid-js"; import { css } from "solid-styled"; import tootStyle from "./toot.module.css"; import MediaViewer from "./MediaViewer"; import { render } from "solid-js/web"; -import { useWindowSize } from "@solid-primitives/resize-observer"; +import { + createElementSize, + useWindowSize, +} from "@solid-primitives/resize-observer"; import { useStore } from "@nanostores/solid"; import { $settings } from "../settings/stores"; +type ElementSize = { width: number; height: number }; + +function constraintedSize( + { width: owidth, height: oheight }: Readonly, // originalSize + { width: mwidth, height: mheight }: Readonly>, // modifier + { width: maxWidth, height: maxHeight }: Readonly, // maxSize +) { + const ySize = owidth + (mwidth ?? 0); + const yScale = ySize > maxWidth ? ySize / maxWidth : 1; + const xSize = oheight + (mheight ?? 0); + const xScale = xSize > maxHeight ? xSize / maxHeight : 1; + + const maxScale = Math.max(yScale, xScale); + const scaledWidth = owidth / maxScale; + const scaledHeight = oheight / maxScale; + + return { + width: scaledWidth, + height: scaledHeight, + }; +} + const MediaAttachmentGrid: Component<{ attachments: mastodon.v1.MediaAttachment[]; }> = (props) => { - let rootRef: HTMLElement; + const [rootRef, setRootRef] = createSignal(); const [viewerIndex, setViewerIndex] = createSignal(); const viewerOpened = () => typeof viewerIndex() !== "undefined"; - const windowSize = useWindowSize(); - const vh35 = () => Math.floor(windowSize.height * 0.35); const settings = useStore($settings); + const windowSize = useWindowSize(); createRenderEffect((lastDispose?: () => void) => { lastDispose?.(); @@ -64,6 +87,22 @@ const MediaAttachmentGrid: Component<{ } }; + const rawElementSize = createElementSize(rootRef); + + const elementWidth = () => rawElementSize.width; + + const itemMaxSize = createMemo(() => { + const ewidth = elementWidth(); + const width = ewidth + ? (ewidth - (columnCount() - 1) * 4) / columnCount() + : 1; + + return { + height: windowSize.height * 0.35, + width, + }; + }); + css` .attachments { column-count: ${columnCount().toString()}; @@ -71,7 +110,7 @@ const MediaAttachmentGrid: Component<{ `; return (
{ if (e.target !== e.currentTarget) { @@ -89,19 +128,33 @@ const MediaAttachmentGrid: Component<{ // before using this. See #37. // TODO: use fast average color to extract accent color const accentColor = item.meta?.colors?.accent; + const { width, height } = item.meta?.small || {}; + + const size = () => { + return constraintedSize( + item.meta?.small || { width: 1, height: 1 }, + { width: 2, height: 2 }, + itemMaxSize(), + ); + }; + switch (item.type) { case "image": return ( {item.description ); case "video": @@ -112,8 +165,8 @@ const MediaAttachmentGrid: Component<{ playsinline={settings().autoPlayVideos ? true : undefined} controls poster={item.previewUrl} - width={item.meta?.small?.width} - height={item.meta?.small?.height} + width={width} + height={height} /> ); case "gifv": @@ -125,8 +178,8 @@ const MediaAttachmentGrid: Component<{ playsinline /* or safari on iOS will play in full-screen */ loop poster={item.previewUrl} - width={item.meta?.small?.width} - height={item.meta?.small?.height} + width={width} + height={height} /> ); diff --git a/src/timelines/toot.module.css b/src/timelines/toot.module.css index f4e458d..e89c1f6 100644 --- a/src/timelines/toot.module.css +++ b/src/timelines/toot.module.css @@ -223,6 +223,7 @@ } .tootAttachmentGrp { + /* Note: MeidaAttachmentGrid has hard-coded layout calcalation */ composes: cardNoPad from "../material/cards.module.css"; margin-top: 1em; margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px); @@ -233,14 +234,12 @@ max-height: 35vh; min-height: 40px; min-width: 40px; - width: auto; - height: auto; object-fit: contain; max-width: 100%; background-color: var(--tutu-color-surface-d); border-radius: 2px; - border: 1px solid var(--tutu-color-on-surface-d); - transition: outline-width 60ms var(--tutu-anim-curve-std), z-index 60ms var(--tutu-anim-curve-std); + border: 1px solid var(--tutu-color-surface-d); + transition: outline-width 60ms var(--tutu-anim-curve-std), border-color 60ms var(--tutu-anim-curve-std); &:hover, &:focus-visible { @@ -248,6 +247,7 @@ /* but our infra is not prepared for this. The average color thing is slow */ /* and we need further managing to control its performance impact. */ outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d)); + border-color: transparent; } } } diff --git a/vite.config.ts b/vite.config.ts index 3b37a34..b575b90 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -62,6 +62,8 @@ export default defineConfig(({ mode }) => { ? `${serverHttpCertBase}.crt` : undefined; + const isTestBuild = ["development", "staging"].includes(mode); + return { plugins: [ suid(), @@ -101,6 +103,10 @@ export default defineConfig(({ mode }) => { } : undefined, }, + esbuild: { + pure: isTestBuild ? undefined : ["console.debug", "console.trace"], + drop: isTestBuild ? undefined : ["debugger"], + }, define: { "import.meta.env.BUILT_AT": `"${new Date().toISOString()}"`, },