From 737d63f88a98d443d8208a9700c9a8845a4854e0 Mon Sep 17 00:00:00 2001 From: thislight Date: Wed, 20 Nov 2024 16:24:57 +0800 Subject: [PATCH 1/6] RegularToot: support content warning --- src/timelines/RegularToot.tsx | 12 ++++ src/timelines/toot-components/TootContent.css | 4 ++ src/timelines/toot-components/TootContent.tsx | 59 +++++++++++++++---- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/timelines/RegularToot.tsx b/src/timelines/RegularToot.tsx index 5a4faa0..bb70372 100644 --- a/src/timelines/RegularToot.tsx +++ b/src/timelines/RegularToot.tsx @@ -5,6 +5,8 @@ import { type JSX, Show, createRenderEffect, + createSignal, + type Setter, } from "solid-js"; import tootStyle from "./toot.module.css"; import { formatRelative } from "date-fns"; @@ -200,6 +202,11 @@ export function findElementActionable( return current; } +function onToggleReveal(setValue: Setter, event: Event) { + event.stopPropagation(); + setValue((x) => !x); +} + /** * Component for a toot. * @@ -239,6 +246,7 @@ const RegularToot: Component = (props) => { const status = () => managed.status; const toot = () => status().reblog ?? status(); const session = useDefaultSession(); + const [reveal, setReveal] = createSignal(false); css` .reply-sep { @@ -326,6 +334,10 @@ const RegularToot: Component = (props) => { emojis={toot().emojis} mentions={toot().mentions} class={cardStyle.cardNoPad} + sensitive={toot().sensitive} + spoilerText={toot().spoilerText} + reveal={reveal()} + onToggleReveal={[onToggleReveal, setReveal]} /> diff --git a/src/timelines/toot-components/TootContent.css b/src/timelines/toot-components/TootContent.css index b5e56e6..79c4ffc 100644 --- a/src/timelines/toot-components/TootContent.css +++ b/src/timelines/toot-components/TootContent.css @@ -4,6 +4,10 @@ margin-right: var(--card-pad, 0); line-height: 1.5; + > .content { + display: contents; + } + & * { user-select: text; } diff --git a/src/timelines/toot-components/TootContent.tsx b/src/timelines/toot-components/TootContent.tsx index fd0dab6..edc3689 100644 --- a/src/timelines/toot-components/TootContent.tsx +++ b/src/timelines/toot-components/TootContent.tsx @@ -5,20 +5,26 @@ import { type JSX, createRenderEffect, createMemo, + Show, } from "solid-js"; import { resolveCustomEmoji } from "../../masto/toot.js"; import { makeAcctText, useDefaultSession } from "../../masto/clients"; import "./TootContent.css"; +import { Button } from "@suid/material"; function preventDefault(event: Event) { event.preventDefault(); } -export type TootContentProps = { +export type TootContentProps = JSX.HTMLAttributes & { source?: string; emojis?: mastodon.v1.CustomEmoji[]; mentions: mastodon.v1.StatusMention[]; -} & JSX.HTMLAttributes; + sensitive?: boolean; + spoilerText?: string; + reveal?: boolean; + onToggleReveal?: JSX.EventHandlerUnion; +}; const TootContent: Component = (oprops) => { const session = useDefaultSession(); @@ -27,23 +33,23 @@ const TootContent: Component = (oprops) => { "emojis", "mentions", "class", + "sensitive", + "spoilerText", + "reveal", + "onToggleReveal", ]); const clientFinder = createMemo(() => session() ? makeAcctText(session()!) : undefined, ); + const shouldRevealContent = () => { + return !props.sensitive || (props.sensitive && props.reveal); + }; + return (
{ - createRenderEffect(() => { - ref.innerHTML = props.source - ? props.emojis - ? resolveCustomEmoji(props.source, props.emojis) - : props.source - : ""; - }); - createRenderEffect(() => { const finder = clientFinder(); for (const mention of props.mentions) { @@ -61,7 +67,38 @@ const TootContent: Component = (oprops) => { }} class={`TootContent ${props.class || ""}`} {...rest} - >
+ > + +
+ { + createRenderEffect(() => { + ref.innerHTML = props.spoilerText + ? props.emojis + ? resolveCustomEmoji(props.spoilerText, props.emojis) + : props.spoilerText + : ""; + }); + }} + > + +
+
+ +
+ createRenderEffect(() => { + ref.innerHTML = props.source + ? props.emojis + ? resolveCustomEmoji(props.source, props.emojis) + : props.source + : ""; + }) + } + >
+
+ ); }; From 6313827b1e69f0db2e39efa01e7efbf565fd8c9f Mon Sep 17 00:00:00 2001 From: thislight Date: Wed, 20 Nov 2024 16:26:05 +0800 Subject: [PATCH 2/6] rename toot-components to toots --- src/timelines/RegularToot.tsx | 6 +++--- src/timelines/{toot-components => toots}/BoostIcon.css | 0 src/timelines/{toot-components => toots}/BoostIcon.tsx | 0 src/timelines/{toot-components => toots}/PreviewCard.css | 0 src/timelines/{toot-components => toots}/PreviewCard.tsx | 0 src/timelines/{toot-components => toots}/TootContent.css | 0 src/timelines/{toot-components => toots}/TootContent.tsx | 2 +- 7 files changed, 4 insertions(+), 4 deletions(-) rename src/timelines/{toot-components => toots}/BoostIcon.css (100%) rename src/timelines/{toot-components => toots}/BoostIcon.tsx (100%) rename src/timelines/{toot-components => toots}/PreviewCard.css (100%) rename src/timelines/{toot-components => toots}/PreviewCard.tsx (100%) rename src/timelines/{toot-components => toots}/TootContent.css (100%) rename src/timelines/{toot-components => toots}/TootContent.tsx (99%) diff --git a/src/timelines/RegularToot.tsx b/src/timelines/RegularToot.tsx index bb70372..6861258 100644 --- a/src/timelines/RegularToot.tsx +++ b/src/timelines/RegularToot.tsx @@ -33,9 +33,9 @@ import MediaAttachmentGrid from "./MediaAttachmentGrid.js"; import { useDateFnLocale } from "../platform/i18n"; import { canShare, share } from "../platform/share"; import { makeAcctText, useDefaultSession } from "../masto/clients"; -import TootContent from "./toot-components/TootContent"; -import BoostIcon from "./toot-components/BoostIcon"; -import PreviewCard from "./toot-components/PreviewCard"; +import TootContent from "./toots/TootContent"; +import BoostIcon from "./toots/BoostIcon"; +import PreviewCard from "./toots/PreviewCard"; type TootActionGroupProps = { onRetoot?: (value: T) => void; diff --git a/src/timelines/toot-components/BoostIcon.css b/src/timelines/toots/BoostIcon.css similarity index 100% rename from src/timelines/toot-components/BoostIcon.css rename to src/timelines/toots/BoostIcon.css diff --git a/src/timelines/toot-components/BoostIcon.tsx b/src/timelines/toots/BoostIcon.tsx similarity index 100% rename from src/timelines/toot-components/BoostIcon.tsx rename to src/timelines/toots/BoostIcon.tsx diff --git a/src/timelines/toot-components/PreviewCard.css b/src/timelines/toots/PreviewCard.css similarity index 100% rename from src/timelines/toot-components/PreviewCard.css rename to src/timelines/toots/PreviewCard.css diff --git a/src/timelines/toot-components/PreviewCard.tsx b/src/timelines/toots/PreviewCard.tsx similarity index 100% rename from src/timelines/toot-components/PreviewCard.tsx rename to src/timelines/toots/PreviewCard.tsx diff --git a/src/timelines/toot-components/TootContent.css b/src/timelines/toots/TootContent.css similarity index 100% rename from src/timelines/toot-components/TootContent.css rename to src/timelines/toots/TootContent.css diff --git a/src/timelines/toot-components/TootContent.tsx b/src/timelines/toots/TootContent.tsx similarity index 99% rename from src/timelines/toot-components/TootContent.tsx rename to src/timelines/toots/TootContent.tsx index edc3689..f81478b 100644 --- a/src/timelines/toot-components/TootContent.tsx +++ b/src/timelines/toots/TootContent.tsx @@ -8,7 +8,7 @@ import { Show, } from "solid-js"; import { resolveCustomEmoji } from "../../masto/toot.js"; -import { makeAcctText, useDefaultSession } from "../../masto/clients"; +import { makeAcctText, useDefaultSession } from "../../masto/clients.js"; import "./TootContent.css"; import { Button } from "@suid/material"; From b1f6033cc80154d60912f9a803e62e39fbb9e4cc Mon Sep 17 00:00:00 2001 From: thislight Date: Wed, 20 Nov 2024 16:33:30 +0800 Subject: [PATCH 3/6] TootContent: localized --- src/timelines/toots/TootContent.tsx | 12 +++++++++++- src/timelines/toots/i18n/en.json | 3 +++ src/timelines/toots/i18n/zh-Hans.json | 3 +++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/timelines/toots/i18n/en.json create mode 100644 src/timelines/toots/i18n/zh-Hans.json diff --git a/src/timelines/toots/TootContent.tsx b/src/timelines/toots/TootContent.tsx index f81478b..7a2286d 100644 --- a/src/timelines/toots/TootContent.tsx +++ b/src/timelines/toots/TootContent.tsx @@ -11,6 +11,7 @@ import { resolveCustomEmoji } from "../../masto/toot.js"; import { makeAcctText, useDefaultSession } from "../../masto/clients.js"; import "./TootContent.css"; import { Button } from "@suid/material"; +import { createTranslator } from "../../platform/i18n.jsx"; function preventDefault(event: Event) { event.preventDefault(); @@ -27,6 +28,15 @@ export type TootContentProps = JSX.HTMLAttributes & { }; const TootContent: Component = (oprops) => { + const [t] = createTranslator( + (code) => + import(`./i18n/${code}.json`) as Promise<{ + default: { + cw: string; + }; + }>, + ); + const session = useDefaultSession(); const [props, rest] = splitProps(oprops, [ "source", @@ -81,7 +91,7 @@ const TootContent: Component = (oprops) => { }); }} > - +
diff --git a/src/timelines/toots/i18n/en.json b/src/timelines/toots/i18n/en.json new file mode 100644 index 0000000..3d75ddd --- /dev/null +++ b/src/timelines/toots/i18n/en.json @@ -0,0 +1,3 @@ +{ + "cw": "\"Content Warning\"" +} \ No newline at end of file diff --git a/src/timelines/toots/i18n/zh-Hans.json b/src/timelines/toots/i18n/zh-Hans.json new file mode 100644 index 0000000..0e64a62 --- /dev/null +++ b/src/timelines/toots/i18n/zh-Hans.json @@ -0,0 +1,3 @@ +{ + "cw": "“内容警告”" +} \ No newline at end of file From 1047a3b10dccf8dbc00f22588ef99650e18dc00d Mon Sep 17 00:00:00 2001 From: thislight Date: Wed, 20 Nov 2024 20:34:30 +0800 Subject: [PATCH 4/6] MediaAttachmentGrid: add a box to each item --- src/timelines/MediaAttachmentGrid.css | 17 +++- src/timelines/MediaAttachmentGrid.tsx | 122 +++++++++++++------------- src/timelines/RegularToot.tsx | 13 +-- 3 files changed, 82 insertions(+), 70 deletions(-) diff --git a/src/timelines/MediaAttachmentGrid.css b/src/timelines/MediaAttachmentGrid.css index 55bb30d..378b091 100644 --- a/src/timelines/MediaAttachmentGrid.css +++ b/src/timelines/MediaAttachmentGrid.css @@ -6,18 +6,27 @@ gap: 4px; contain: layout style; - > :where(img, video) { + &.sensitive>.cell> :where(img, video) { + filter: blur(20px) saturate(0.2); + } + + >.cell> :where(img, video) { + object-fit: contain; + width: 100%; + height: 100%; + } + + >.cell { max-height: 35vh; min-height: 40px; min-width: 40px; - object-fit: contain; max-width: 100%; + contain: strict; + content-visibility: auto; background-color: var(--media-color-accent, 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 { diff --git a/src/timelines/MediaAttachmentGrid.tsx b/src/timelines/MediaAttachmentGrid.tsx index 2b9db57..8fec903 100644 --- a/src/timelines/MediaAttachmentGrid.tsx +++ b/src/timelines/MediaAttachmentGrid.tsx @@ -1,7 +1,6 @@ import type { mastodon } from "masto"; import { type Component, - For, Index, Match, Switch, @@ -44,8 +43,15 @@ function constraintedSize( }; } +function isolateCallback(event: Event) { + if (event.target !== event.currentTarget) { + event.stopPropagation(); + } +} + const MediaAttachmentGrid: Component<{ attachments: mastodon.v1.MediaAttachment[]; + sensitive?: boolean; }> = (props) => { const [rootRef, setRootRef] = createSignal(); const [viewerIndex, setViewerIndex] = createSignal(); @@ -53,14 +59,13 @@ const MediaAttachmentGrid: Component<{ const settings = useStore($settings); const windowSize = useWindowSize(); - createRenderEffect((lastDispose?: () => void) => { - lastDispose?.(); + createRenderEffect(() => { const vidx = viewerIndex(); if (typeof vidx === "undefined") return; const container = document.createElement("div"); container.setAttribute("role", "presentation"); document.body.appendChild(container); - return render(() => { + const dispose = render(() => { onCleanup(() => { document.body.removeChild(container); }); @@ -75,6 +80,8 @@ const MediaAttachmentGrid: Component<{ /> ); }, container); + + onCleanup(dispose); }); const openViewerFor = (index: number) => { @@ -131,69 +138,62 @@ const MediaAttachmentGrid: Component<{
{ - if (e.target !== e.currentTarget) { - e.stopImmediatePropagation(); - } + classList={{ + sensitive: props.sensitive, }} + style={{ "column-count": columnCount() }} + onClick={isolateCallback} > {(item, index) => { const itemType = () => item().type; return ( - - - {item().description - - - - - - - - - + ); }} diff --git a/src/timelines/RegularToot.tsx b/src/timelines/RegularToot.tsx index 6861258..d98de09 100644 --- a/src/timelines/RegularToot.tsx +++ b/src/timelines/RegularToot.tsx @@ -47,7 +47,7 @@ type TootActionGroupProps = { ) => void; }; -type TootCardProps = { +type RegularTootProps = { status: mastodon.v1.Status; actionable?: boolean; evaluated?: boolean; @@ -235,7 +235,7 @@ function onToggleReveal(setValue: Setter, event: Event) { * You can extract the intent from the attributes of the "actionable" element. * The action type is the dataset's `action`. */ -const RegularToot: Component = (props) => { +const RegularToot: Component = (props) => { let rootRef: HTMLElement; const [managed, managedActionGroup, rest] = splitProps( props, @@ -293,7 +293,7 @@ const RegularToot: Component = (props) => { return ( <> -
= (props) => { 0}> - + = (props) => { /> -
+ ); }; From 8cd95b9e90f0db1c8bf4b4a09c5e86d37f59836a Mon Sep 17 00:00:00 2001 From: thislight Date: Wed, 20 Nov 2024 21:09:14 +0800 Subject: [PATCH 5/6] MediaAttachmentGrid: support click to reveal --- src/timelines/MediaAttachmentGrid.css | 22 ++++++++++++---------- src/timelines/MediaAttachmentGrid.tsx | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/timelines/MediaAttachmentGrid.css b/src/timelines/MediaAttachmentGrid.css index 378b091..a26085c 100644 --- a/src/timelines/MediaAttachmentGrid.css +++ b/src/timelines/MediaAttachmentGrid.css @@ -6,16 +6,6 @@ gap: 4px; contain: layout style; - &.sensitive>.cell> :where(img, video) { - filter: blur(20px) saturate(0.2); - } - - >.cell> :where(img, video) { - object-fit: contain; - width: 100%; - height: 100%; - } - >.cell { max-height: 35vh; min-height: 40px; @@ -33,5 +23,17 @@ outline: 8px solid var(--media-color-accent, var(--tutu-color-surface-d)); border-color: var(--media-color-accent, var(--tutu-color-surface-d)); } + + & > :where(img, video, .sensitive-placeholder) { + object-fit: contain; + width: 100%; + height: 100%; + } + + & > .sensitive-placeholder { + display: flex; + align-items: center; + justify-content: center; + } } } \ No newline at end of file diff --git a/src/timelines/MediaAttachmentGrid.tsx b/src/timelines/MediaAttachmentGrid.tsx index 8fec903..948bb7b 100644 --- a/src/timelines/MediaAttachmentGrid.tsx +++ b/src/timelines/MediaAttachmentGrid.tsx @@ -8,6 +8,7 @@ import { createRenderEffect, createSignal, onCleanup, + untrack, } from "solid-js"; import MediaViewer from "./MediaViewer"; import { render } from "solid-js/web"; @@ -20,6 +21,8 @@ import { $settings } from "../settings/stores"; import { averageColorHex } from "../platform/blurhash"; import "./MediaAttachmentGrid.css"; import cardStyle from "../material/cards.module.css"; +import { Preview } from "@suid/icons-material"; +import { IconButton } from "@suid/material"; type ElementSize = { width: number; height: number }; @@ -58,6 +61,7 @@ const MediaAttachmentGrid: Component<{ const viewerOpened = () => typeof viewerIndex() !== "undefined"; const settings = useStore($settings); const windowSize = useWindowSize(); + const [reveal, setReveal] = createSignal([] as number[]); createRenderEffect(() => { const vidx = viewerIndex(); @@ -134,6 +138,16 @@ const MediaAttachmentGrid: Component<{ accentColor ? { "--media-color-accent": accentColor } : {}, ); }; + + const isReveal = (idx: number) => { + return reveal().includes(idx); + }; + + const addReveal = (idx: number) => { + if (!untrack(() => isReveal(idx))) { + setReveal((x) => [...x, idx]); + } + }; return (
+ +
+ + + +
+
Date: Wed, 20 Nov 2024 21:17:22 +0800 Subject: [PATCH 6/6] RegularToot: only show preview if reveal --- src/timelines/RegularToot.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/timelines/RegularToot.tsx b/src/timelines/RegularToot.tsx index d98de09..07541ad 100644 --- a/src/timelines/RegularToot.tsx +++ b/src/timelines/RegularToot.tsx @@ -339,7 +339,11 @@ const RegularToot: Component = (props) => { reveal={reveal()} onToggleReveal={[onToggleReveal, setReveal]} /> - + 0}>