Compare commits

..

No commits in common. "726abbe893b579fe24b8c69270048c03a2a47f9e" and "66bded813d22a4cf2f99110fd07f9d7655219d3b" have entirely different histories.

8 changed files with 33 additions and 114 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -36,7 +36,6 @@
"@solid-primitives/i18n": "^2.1.1", "@solid-primitives/i18n": "^2.1.1",
"@solid-primitives/intersection-observer": "^2.1.6", "@solid-primitives/intersection-observer": "^2.1.6",
"@solid-primitives/map": "^0.4.13", "@solid-primitives/map": "^0.4.13",
"@solid-primitives/page-visibility": "^2.0.17",
"@solid-primitives/resize-observer": "^2.0.26", "@solid-primitives/resize-observer": "^2.0.26",
"@solidjs/router": "^0.14.10", "@solidjs/router": "^0.14.10",
"@suid/icons-material": "^0.8.1", "@suid/icons-material": "^0.8.1",

View file

@ -1,4 +1,3 @@
import { usePageVisibility } from "@solid-primitives/page-visibility";
import { import {
Accessor, Accessor,
createContext, createContext,
@ -16,29 +15,18 @@ export const TimeSourceProvider = TimeSourceContext.Provider;
export function createTimeSource() { export function createTimeSource() {
let id: ReturnType<typeof setTimeout> | undefined; let id: ReturnType<typeof setTimeout> | undefined;
const [get, set] = createSignal(new Date()); const [get, set] = createSignal(new Date());
const visible = usePageVisibility();
const cancelTimer = () => { createRenderEffect(() =>
if (typeof id !== "undefined") { untrack(() => {
clearInterval(id);
}
id = undefined;
};
const resetTimer = () => {
cancelTimer();
set(new Date());
id = setTimeout(() => { id = setTimeout(() => {
set(new Date()); set(new Date());
}, 30 * 1000); // refresh rate: 30s }, 30 * 1000);
}; }),
);
createRenderEffect(() => { onCleanup(() => {
onCleanup(cancelTimer); if (typeof id !== "undefined") {
if (visible()) { clearInterval(id);
resetTimer();
} else {
console.debug("createTimeSource: page is invisible, cancel the timer")
} }
}); });

View file

@ -13,7 +13,6 @@ import {
AppBar, AppBar,
Avatar, Avatar,
Button, Button,
CircularProgress,
Divider, Divider,
IconButton, IconButton,
ListItemIcon, ListItemIcon,
@ -103,7 +102,7 @@ const Profile: Component = () => {
const avatarImg = () => profile()?.avatar; const avatarImg = () => profile()?.avatar;
const displayName = () => const displayName = () =>
resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []); resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []);
const fullUsername = () => (profile()?.acct ? `@${profile()!.acct!}` : ""); // TODO: full user name const fullUsername = () => `@${profile()?.acct ?? ""}`; // TODO: full user name
const description = () => profile()?.note; const description = () => profile()?.note;
css` css`
@ -232,12 +231,7 @@ const Profile: Component = () => {
<ListItemText>Mention {profile()?.displayName || ""}...</ListItemText> <ListItemText>Mention {profile()?.displayName || ""}...</ListItemText>
</MenuItem> </MenuItem>
<Divider /> <Divider />
<MenuItem <MenuItem component={"a"} href={profile()?.url} target="_blank" rel="noopener noreferrer">
component={"a"}
href={profile()?.url}
target="_blank"
rel="noopener noreferrer"
>
<ListItemIcon> <ListItemIcon>
<OpenInBrowser /> <OpenInBrowser />
</ListItemIcon> </ListItemIcon>
@ -313,7 +307,6 @@ const Profile: Component = () => {
createRenderEffect(() => (e.innerHTML = description() || "")) createRenderEffect(() => (e.innerHTML = description() || ""))
} }
></div> ></div>
<table class="acct-fields"> <table class="acct-fields">
<tbody> <tbody>
<For each={profile()?.fields ?? []}> <For each={profile()?.fields ?? []}>
@ -372,11 +365,8 @@ const Profile: Component = () => {
size="large" size="large"
color="primary" color="primary"
onClick={[refetchRecentToots, "prev"]} onClick={[refetchRecentToots, "prev"]}
disabled={recentTootChunk.loading}
> >
<Show when={recentTootChunk.loading} fallback={<ExpandMore />}> <ExpandMore />
<CircularProgress sx={{ width: "24px", height: "24px" }} />
</Show>
</IconButton> </IconButton>
</div> </div>
</Show> </Show>

View file

@ -249,6 +249,7 @@ const Home: ParentComponent = (props) => {
<div> <div>
<TrendTimelinePanel <TrendTimelinePanel
client={client()} client={client()}
prefetch={prefetching()}
openFullScreenToot={openFullScreenToot} openFullScreenToot={openFullScreenToot}
/> />
</div> </div>

View file

@ -2,52 +2,29 @@ import type { mastodon } from "masto";
import { import {
type Component, type Component,
For, For,
createMemo, createEffect,
createRenderEffect, createRenderEffect,
createSignal, createSignal,
onCleanup, onCleanup,
onMount,
} from "solid-js"; } from "solid-js";
import { css } from "solid-styled"; import { css } from "solid-styled";
import tootStyle from "./toot.module.css"; import tootStyle from "./toot.module.css";
import MediaViewer from "./MediaViewer"; import MediaViewer from "./MediaViewer";
import { render } from "solid-js/web"; import { render } from "solid-js/web";
import { import { useWindowSize } from "@solid-primitives/resize-observer";
createElementSize,
useWindowSize,
} from "@solid-primitives/resize-observer";
import { useStore } from "@nanostores/solid"; import { useStore } from "@nanostores/solid";
import { $settings } from "../settings/stores"; import { $settings } from "../settings/stores";
type ElementSize = { width: number; height: number };
function constraintedSize(
{ width: owidth, height: oheight }: Readonly<ElementSize>, // originalSize
{ width: mwidth, height: mheight }: Readonly<Partial<ElementSize>>, // modifier
{ width: maxWidth, height: maxHeight }: Readonly<ElementSize>, // 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<{ const MediaAttachmentGrid: Component<{
attachments: mastodon.v1.MediaAttachment[]; attachments: mastodon.v1.MediaAttachment[];
}> = (props) => { }> = (props) => {
const [rootRef, setRootRef] = createSignal<HTMLElement>(); let rootRef: HTMLElement;
const [viewerIndex, setViewerIndex] = createSignal<number>(); const [viewerIndex, setViewerIndex] = createSignal<number>();
const viewerOpened = () => typeof viewerIndex() !== "undefined"; const viewerOpened = () => typeof viewerIndex() !== "undefined";
const settings = useStore($settings);
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const vh35 = () => Math.floor(windowSize.height * 0.35);
const settings = useStore($settings);
createRenderEffect((lastDispose?: () => void) => { createRenderEffect((lastDispose?: () => void) => {
lastDispose?.(); lastDispose?.();
@ -87,22 +64,6 @@ 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` css`
.attachments { .attachments {
column-count: ${columnCount().toString()}; column-count: ${columnCount().toString()};
@ -110,7 +71,7 @@ const MediaAttachmentGrid: Component<{
`; `;
return ( return (
<section <section
ref={setRootRef} ref={rootRef!}
class={[tootStyle.tootAttachmentGrp, "attachments"].join(" ")} class={[tootStyle.tootAttachmentGrp, "attachments"].join(" ")}
onClick={(e) => { onClick={(e) => {
if (e.target !== e.currentTarget) { if (e.target !== e.currentTarget) {
@ -128,33 +89,19 @@ const MediaAttachmentGrid: Component<{
// before using this. See #37. // before using this. See #37.
// TODO: use fast average color to extract accent color // TODO: use fast average color to extract accent color
const accentColor = item.meta?.colors?.accent; 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) { switch (item.type) {
case "image": case "image":
return ( return (
<img <img
src={item.previewUrl} src={item.previewUrl}
width={width} width={item.meta?.small?.width}
height={height} height={item.meta?.small?.height}
alt={item.description || undefined} alt={item.description || undefined}
onClick={[openViewerFor, index()]} onClick={[openViewerFor, index()]}
loading="lazy" loading="lazy"
style={Object.assign( style={
{ accentColor ? { "--media-color-accent": accentColor } : {}
width: `${size().width}px`, }
height: `${size().height}px`,
},
accentColor ? { "--media-color-accent": accentColor } : {},
)}
></img> ></img>
); );
case "video": case "video":
@ -165,8 +112,8 @@ const MediaAttachmentGrid: Component<{
playsinline={settings().autoPlayVideos ? true : undefined} playsinline={settings().autoPlayVideos ? true : undefined}
controls controls
poster={item.previewUrl} poster={item.previewUrl}
width={width} width={item.meta?.small?.width}
height={height} height={item.meta?.small?.height}
/> />
); );
case "gifv": case "gifv":
@ -178,8 +125,8 @@ const MediaAttachmentGrid: Component<{
playsinline /* or safari on iOS will play in full-screen */ playsinline /* or safari on iOS will play in full-screen */
loop loop
poster={item.previewUrl} poster={item.previewUrl}
width={width} width={item.meta?.small?.width}
height={height} height={item.meta?.small?.height}
/> />
); );

View file

@ -223,7 +223,6 @@
} }
.tootAttachmentGrp { .tootAttachmentGrp {
/* Note: MeidaAttachmentGrid has hard-coded layout calcalation */
composes: cardNoPad from "../material/cards.module.css"; composes: cardNoPad from "../material/cards.module.css";
margin-top: 1em; margin-top: 1em;
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px); margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
@ -234,12 +233,14 @@
max-height: 35vh; max-height: 35vh;
min-height: 40px; min-height: 40px;
min-width: 40px; min-width: 40px;
width: auto;
height: auto;
object-fit: contain; object-fit: contain;
max-width: 100%; max-width: 100%;
background-color: var(--tutu-color-surface-d); background-color: var(--tutu-color-surface-d);
border-radius: 2px; border-radius: 2px;
border: 1px solid var(--tutu-color-surface-d); border: 1px solid var(--tutu-color-on-surface-d);
transition: outline-width 60ms var(--tutu-anim-curve-std), border-color 60ms var(--tutu-anim-curve-std); transition: outline-width 60ms var(--tutu-anim-curve-std), z-index 60ms var(--tutu-anim-curve-std);
&:hover, &:hover,
&:focus-visible { &:focus-visible {
@ -247,7 +248,6 @@
/* but our infra is not prepared for this. The average color thing is slow */ /* but our infra is not prepared for this. The average color thing is slow */
/* and we need further managing to control its performance impact. */ /* and we need further managing to control its performance impact. */
outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d)); outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d));
border-color: transparent;
} }
} }
} }

View file

@ -62,8 +62,6 @@ export default defineConfig(({ mode }) => {
? `${serverHttpCertBase}.crt` ? `${serverHttpCertBase}.crt`
: undefined; : undefined;
const isTestBuild = ["development", "staging"].includes(mode);
return { return {
plugins: [ plugins: [
suid(), suid(),
@ -103,10 +101,6 @@ export default defineConfig(({ mode }) => {
} }
: undefined, : undefined,
}, },
esbuild: {
pure: isTestBuild ? undefined : ["console.debug", "console.trace"],
drop: isTestBuild ? undefined : ["debugger"],
},
define: { define: {
"import.meta.env.BUILT_AT": `"${new Date().toISOString()}"`, "import.meta.env.BUILT_AT": `"${new Date().toISOString()}"`,
}, },