Compare commits

..

No commits in common. "4d9c2b3aa8f4f25342afcd10809965b937e686eb" and "2aa2cf21da3ed75a6b6e998cee2dcc89cf179761" have entirely different histories.

8 changed files with 144 additions and 202 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -17,16 +17,16 @@
"@suid/vite-plugin": "^0.3.1", "@suid/vite-plugin": "^0.3.1",
"@types/hammerjs": "^2.0.46", "@types/hammerjs": "^2.0.46",
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"postcss": "^8.4.49", "postcss": "^8.4.47",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^5.4.11", "vite": "^5.4.10",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-plugin-pwa": "^0.20.5", "vite-plugin-pwa": "^0.20.5",
"vite-plugin-solid": "^2.10.2", "vite-plugin-solid": "^2.10.2",
"vite-plugin-solid-styled": "^0.11.1", "vite-plugin-solid-styled": "^0.11.1",
"workbox-build": "^7.3.0", "workbox-build": "^7.3.0",
"wrangler": "^3.86.1" "wrangler": "^3.84.1"
}, },
"dependencies": { "dependencies": {
"@formatjs/intl-localematcher": "^0.5.7", "@formatjs/intl-localematcher": "^0.5.7",
@ -38,7 +38,7 @@
"@solid-primitives/map": "^0.4.13", "@solid-primitives/map": "^0.4.13",
"@solid-primitives/page-visibility": "^2.0.17", "@solid-primitives/page-visibility": "^2.0.17",
"@solid-primitives/resize-observer": "^2.0.26", "@solid-primitives/resize-observer": "^2.0.26",
"@solidjs/router": "^0.15.1", "@solidjs/router": "^0.14.10",
"@suid/icons-material": "^0.8.1", "@suid/icons-material": "^0.8.1",
"@suid/material": "^0.18.0", "@suid/material": "^0.18.0",
"blurhash": "^2.0.5", "blurhash": "^2.0.5",

View file

@ -1,27 +0,0 @@
.MediaAttachmentGrid {
/* Note: MeidaAttachmentGrid has hard-coded layout calcalation */
margin-top: 1em;
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
margin-right: var(--card-pad, 0);
gap: 4px;
> :where(img, video) {
max-height: 35vh;
min-height: 40px;
min-width: 40px;
object-fit: contain;
max-width: 100%;
background-color: var(--tutu-color-surface-d);
border-radius: 2px;
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);
contain: strict;
content-visibility: auto;
&:hover,
&:focus-visible {
outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d));
border-color: var(--media-color-accent, var(--tutu-color-surface-d));
}
}
}

View file

@ -10,6 +10,8 @@ import {
createSignal, createSignal,
onCleanup, onCleanup,
} from "solid-js"; } from "solid-js";
import { css } from "solid-styled";
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 {
@ -19,8 +21,6 @@ import {
import { useStore } from "@nanostores/solid"; import { useStore } from "@nanostores/solid";
import { $settings } from "../settings/stores"; import { $settings } from "../settings/stores";
import { averageColorHex } from "../platform/blurhash"; import { averageColorHex } from "../platform/blurhash";
import "./MediaAttachmentGrid.css";
import cardStyle from "../material/cards.module.css";
type ElementSize = { width: number; height: number }; type ElementSize = { width: number; height: number };
@ -114,6 +114,13 @@ const MediaAttachmentGrid: Component<{
itemMaxSize(), itemMaxSize(),
); );
// I don't know why mastodon does not return this
// and the condition for it to return this.
// Anyway, It is useless now.
// My hope is the FastAverageColor, but
// we may need better tool to manage the performance impact
// before using this. See #37.
// TODO: use fast average color to extract accent color
const accentColor = const accentColor =
item.meta?.colors?.accent ?? item.meta?.colors?.accent ??
(item.blurhash ? averageColorHex(item.blurhash) : undefined); (item.blurhash ? averageColorHex(item.blurhash) : undefined);
@ -127,11 +134,16 @@ const MediaAttachmentGrid: Component<{
accentColor ? { "--media-color-accent": accentColor } : {}, accentColor ? { "--media-color-accent": accentColor } : {},
); );
}; };
css`
.attachments {
column-count: ${columnCount().toString()};
}
`;
return ( return (
<section <section
ref={setRootRef} ref={setRootRef}
class={`MediaAttachmentGrid ${cardStyle.cardNoPad}`} class={[tootStyle.tootAttachmentGrp, "attachments"].join(" ")}
style={{ "column-count": columnCount() }}
onClick={(e) => { onClick={(e) => {
if (e.target !== e.currentTarget) { if (e.target !== e.currentTarget) {
e.stopImmediatePropagation(); e.stopImmediatePropagation();

View file

@ -5,11 +5,13 @@ import {
type JSX, type JSX,
Show, Show,
createRenderEffect, createRenderEffect,
createEffect,
createMemo,
} from "solid-js"; } from "solid-js";
import tootStyle from "./toot.module.css"; import tootStyle from "./toot.module.css";
import { formatRelative } from "date-fns"; import { formatRelative } from "date-fns";
import Img from "../material/Img.js"; import Img from "../material/Img.js";
import { Body2 } from "../material/typography.js"; import { Body1, Body2, Title } from "../material/typography.js";
import { css } from "solid-styled"; import { css } from "solid-styled";
import { import {
BookmarkAddOutlined, BookmarkAddOutlined,
@ -28,12 +30,13 @@ import { Divider } from "@suid/material";
import cardStyle from "../material/cards.module.css"; import cardStyle from "../material/cards.module.css";
import Button from "../material/Button.js"; import Button from "../material/Button.js";
import MediaAttachmentGrid from "./MediaAttachmentGrid.js"; import MediaAttachmentGrid from "./MediaAttachmentGrid.js";
import Color from "colorjs.io";
import { useDateFnLocale } from "../platform/i18n"; import { useDateFnLocale } from "../platform/i18n";
import { canShare, share } from "../platform/share"; import { canShare, share } from "../platform/share";
import { makeAcctText, useDefaultSession } from "../masto/clients"; import { makeAcctText, useDefaultSession } from "../masto/clients";
import TootContent from "./toot-components/TootContent"; import TootContent from "./toot-components/TootContent";
import BoostIcon from "./toot-components/BoostIcon"; import BoostIcon from "./toot-components/BoostIcon";
import PreviewCard from "./toot-components/PreviewCard"; import { averageColorHex } from "../platform/blurhash";
type TootActionGroupProps<T extends mastodon.v1.Status> = { type TootActionGroupProps<T extends mastodon.v1.Status> = {
onRetoot?: (value: T) => void; onRetoot?: (value: T) => void;
@ -183,6 +186,95 @@ function TootAuthorGroup(
); );
} }
export function TootPreviewCard(props: {
src: mastodon.v1.PreviewCard;
alwaysCompact?: boolean;
}) {
let root: HTMLAnchorElement;
createEffect(() => {
if (props.alwaysCompact) {
root.classList.add(tootStyle.compact);
return;
}
if (!props.src.width) return;
const width = root.getBoundingClientRect().width;
if (width > props.src.width) {
root.classList.add(tootStyle.compact);
} else {
root.classList.remove(tootStyle.compact);
}
});
const imgAverageColor = createMemo(() => {
if (!props.src.image) return;
return new Color(averageColorHex(props.src.blurhash));
});
const prefersWhiteText = createMemo(() => {
const oc = imgAverageColor();
if (!oc) return;
const colorWhite = new Color("white");
return colorWhite.luminance / oc.luminance > 3.5;
});
const focusSurfaceColor = createMemo(() => {
const oc = imgAverageColor();
if (!oc) return;
if (prefersWhiteText()) {
return new Color(oc).darken(0.2);
} else {
return new Color(oc).lighten(0.2);
}
});
const textColorName = createMemo(() => {
const useWhiteText = prefersWhiteText();
if (typeof useWhiteText === "undefined") {
return;
}
return useWhiteText ? "white" : "black";
});
const secondaryTextColor = createMemo(() => {
const tcn = textColorName();
if (!tcn) return;
const tc = new Color(tcn);
tc.alpha = 0.75;
return tc;
});
return (
<a
ref={root!}
class={tootStyle.previewCard}
href={props.src.url}
target="_blank"
referrerPolicy="unsafe-url"
style={{
"--tutu-color-surface": imgAverageColor()?.toString(),
"--tutu-color-surface-d": focusSurfaceColor()?.toString(),
"--tutu-color-on-surface": textColorName(),
"--tutu-color-secondary-text-on-surface":
secondaryTextColor()?.toString(),
}}
>
<Show when={props.src.image}>
<img
crossOrigin="anonymous"
src={props.src.image!}
width={props.src.width || undefined}
height={props.src.height || undefined}
loading="lazy"
/>
</Show>
<Title component="h1">{props.src.title}</Title>
<Body1 component="p">{props.src.description}</Body1>
</a>
);
}
/** /**
* find bottom-to-top the element with `data-action`. * find bottom-to-top the element with `data-action`.
*/ */
@ -328,7 +420,7 @@ const RegularToot: Component<TootCardProps> = (props) => {
class={tootStyle.tootContent} class={tootStyle.tootContent}
/> />
<Show when={toot().card}> <Show when={toot().card}>
<PreviewCard src={toot().card!} /> <TootPreviewCard src={toot().card!} />
</Show> </Show>
<Show when={toot().mediaAttachments.length > 0}> <Show when={toot().mediaAttachments.length > 0}>
<MediaAttachmentGrid attachments={toot().mediaAttachments} /> <MediaAttachmentGrid attachments={toot().mediaAttachments} />

View file

@ -1,67 +0,0 @@
.PreviewCard {
display: block;
border: 1px solid #eeeeee;
background-color: var(--tutu-color-surface);
text-decoration: none;
border-radius: 4px;
overflow: hidden;
margin-top: 1em;
margin-bottom: 1.5em;
color: var(--tutu-color-secondary-text-on-surface);
transition: color 220ms var(--tutu-anim-curve-std), background-color 220ms var(--tutu-anim-curve-std);
padding-bottom: 8px;
overflow: hidden;
z-index: 1;
position: relative;
>img {
background-color: #eeeeee;
max-width: 100%;
height: auto;
}
&:hover,
&:focus-visible {
background-color: var(--tutu-color-surface-d);
color: var(--tutu-color-on-surface);
>h1 {
text-decoration: underline;
}
}
>h1 {
color: var(--tutu-color-on-surface);
max-height: calc(4 * var(--title-line-height) * var(--title-size));
}
>p {
max-height: calc(8 * var(--body-line-height) * var(--body-size));
}
>h1,
>p {
margin-left: 16px;
margin-right: 16px;
overflow: hidden;
text-overflow: ellipsis;
}
&.compact {
display: grid;
grid-template-columns: minmax(10%, 30%) 1fr;
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
>img:first-child {
grid-row: 1 / 3;
object-fit: contain;
}
>h1,
>p {
margin-right: 0;
}
}
}

View file

@ -1,97 +0,0 @@
import Color from "colorjs.io";
import type { mastodon } from "masto";
import { createEffect, createMemo, Show } from "solid-js";
import { Title, Body1 } from "../../material/typography";
import { averageColorHex } from "../../platform/blurhash";
import "./PreviewCard.css";
export function PreviewCard(props: {
src: mastodon.v1.PreviewCard;
alwaysCompact?: boolean;
}) {
let root: HTMLAnchorElement;
createEffect(() => {
if (props.alwaysCompact) {
root.classList.add("compact");
return;
}
if (!props.src.width) return;
const width = root.getBoundingClientRect().width;
if (width > props.src.width) {
root.classList.add("compact");
} else {
root.classList.remove("compact");
}
});
const imgAverageColor = createMemo(() => {
if (!props.src.image) return;
return new Color(averageColorHex(props.src.blurhash));
});
const prefersWhiteText = createMemo(() => {
const oc = imgAverageColor();
if (!oc) return;
const colorWhite = new Color("white");
return colorWhite.luminance / oc.luminance > 3.5;
});
const focusSurfaceColor = createMemo(() => {
const oc = imgAverageColor();
if (!oc) return;
if (prefersWhiteText()) {
return new Color(oc).darken(0.2);
} else {
return new Color(oc).lighten(0.2);
}
});
const textColorName = createMemo(() => {
const useWhiteText = prefersWhiteText();
if (typeof useWhiteText === "undefined") {
return;
}
return useWhiteText ? "white" : "black";
});
const secondaryTextColor = createMemo(() => {
const tcn = textColorName();
if (!tcn) return;
const tc = new Color(tcn);
tc.alpha = 0.75;
return tc;
});
return (
<a
ref={root!}
class={"PreviewCard"}
href={props.src.url}
target="_blank"
referrerPolicy="unsafe-url"
style={{
"--tutu-color-surface": imgAverageColor()?.toString(),
"--tutu-color-surface-d": focusSurfaceColor()?.toString(),
"--tutu-color-on-surface": textColorName(),
"--tutu-color-secondary-text-on-surface":
secondaryTextColor()?.toString(),
}}
>
<Show when={props.src.image}>
<img
crossOrigin="anonymous"
src={props.src.image!}
width={props.src.width || undefined}
height={props.src.height || undefined}
loading="lazy"
/>
</Show>
<Title component="h1">{props.src.title}</Title>
<Body1 component="p">{props.src.description}</Body1>
</a>
);
}
export default PreviewCard;

View file

@ -238,6 +238,35 @@
} }
} }
.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);
margin-right: var(--card-pad, 0);
gap: 4px;
> :where(img, video) {
max-height: 35vh;
min-height: 40px;
min-width: 40px;
object-fit: contain;
max-width: 100%;
background-color: var(--tutu-color-surface-d);
border-radius: 2px;
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);
contain: strict;
content-visibility: auto;
&:hover,
&:focus-visible {
outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d));
border: none;
}
}
}
.tootBottomActionGrp { .tootBottomActionGrp {
composes: cardGutSkip from "../material/cards.module.css"; composes: cardGutSkip from "../material/cards.module.css";
padding-block: calc((var(--card-gut) - 10px) / 2); padding-block: calc((var(--card-gut) - 10px) / 2);