Compare commits
No commits in common. "8d8d2a8fb153284eba2a6d10c251ba3e749308d3" and "cff0c2880ad19f5cad78c830e62e1ce72ed6a1cb" have entirely different histories.
8d8d2a8fb1
...
cff0c2880a
12 changed files with 142 additions and 255 deletions
|
@ -6,34 +6,23 @@
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
|
|
||||||
>.cell {
|
> :where(img, video) {
|
||||||
max-height: 35vh;
|
max-height: 35vh;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
|
object-fit: contain;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
contain: strict;
|
|
||||||
content-visibility: auto;
|
|
||||||
background-color: var(--media-color-accent, var(--tutu-color-surface-d));
|
background-color: var(--media-color-accent, 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-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), border-color 60ms var(--tutu-anim-curve-std);
|
||||||
|
contain: strict;
|
||||||
|
content-visibility: auto;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
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: 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import type { mastodon } from "masto";
|
import type { mastodon } from "masto";
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
|
For,
|
||||||
Index,
|
Index,
|
||||||
Match,
|
Match,
|
||||||
Switch,
|
Switch,
|
||||||
|
@ -8,7 +9,6 @@ import {
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
createSignal,
|
createSignal,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
untrack,
|
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import MediaViewer from "./MediaViewer";
|
import MediaViewer from "./MediaViewer";
|
||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
|
@ -21,8 +21,6 @@ import { $settings } from "../settings/stores";
|
||||||
import { averageColorHex } from "../platform/blurhash";
|
import { averageColorHex } from "../platform/blurhash";
|
||||||
import "./MediaAttachmentGrid.css";
|
import "./MediaAttachmentGrid.css";
|
||||||
import cardStyle from "../material/cards.module.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 };
|
type ElementSize = { width: number; height: number };
|
||||||
|
|
||||||
|
@ -46,30 +44,23 @@ function constraintedSize(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isolateCallback(event: Event) {
|
|
||||||
if (event.target !== event.currentTarget) {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MediaAttachmentGrid: Component<{
|
const MediaAttachmentGrid: Component<{
|
||||||
attachments: mastodon.v1.MediaAttachment[];
|
attachments: mastodon.v1.MediaAttachment[];
|
||||||
sensitive?: boolean;
|
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const [rootRef, setRootRef] = createSignal<HTMLElement>();
|
const [rootRef, setRootRef] = createSignal<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 settings = useStore($settings);
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const [reveal, setReveal] = createSignal([] as number[]);
|
|
||||||
|
|
||||||
createRenderEffect(() => {
|
createRenderEffect((lastDispose?: () => void) => {
|
||||||
|
lastDispose?.();
|
||||||
const vidx = viewerIndex();
|
const vidx = viewerIndex();
|
||||||
if (typeof vidx === "undefined") return;
|
if (typeof vidx === "undefined") return;
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.setAttribute("role", "presentation");
|
container.setAttribute("role", "presentation");
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
const dispose = render(() => {
|
return render(() => {
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
});
|
});
|
||||||
|
@ -84,8 +75,6 @@ const MediaAttachmentGrid: Component<{
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, container);
|
}, container);
|
||||||
|
|
||||||
onCleanup(dispose);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const openViewerFor = (index: number) => {
|
const openViewerFor = (index: number) => {
|
||||||
|
@ -138,88 +127,73 @@ const MediaAttachmentGrid: Component<{
|
||||||
accentColor ? { "--media-color-accent": accentColor } : {},
|
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 (
|
return (
|
||||||
<section
|
<section
|
||||||
ref={setRootRef}
|
ref={setRootRef}
|
||||||
class={`MediaAttachmentGrid ${cardStyle.cardNoPad}`}
|
class={`MediaAttachmentGrid ${cardStyle.cardNoPad}`}
|
||||||
classList={{
|
|
||||||
sensitive: props.sensitive,
|
|
||||||
}}
|
|
||||||
style={{ "column-count": columnCount() }}
|
style={{ "column-count": columnCount() }}
|
||||||
onClick={isolateCallback}
|
onClick={(e) => {
|
||||||
|
if (e.target !== e.currentTarget) {
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Index each={props.attachments}>
|
<Index each={props.attachments}>
|
||||||
{(item, index) => {
|
{(item, index) => {
|
||||||
const itemType = () => item().type;
|
const itemType = () => item().type;
|
||||||
return (
|
return (
|
||||||
<div
|
|
||||||
class="cell"
|
|
||||||
role="presentation"
|
|
||||||
style={itemStyle(item())}
|
|
||||||
data-sort={index}
|
|
||||||
data-media-type={item().type}
|
|
||||||
>
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.sensitive && !isReveal(index)}>
|
|
||||||
<div class="sensitive-placeholder">
|
|
||||||
<IconButton
|
|
||||||
color="inherit"
|
|
||||||
size="large"
|
|
||||||
onClick={[addReveal, index]}
|
|
||||||
aria-label="Reveal this media"
|
|
||||||
>
|
|
||||||
<Preview />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={itemType() === "image"}>
|
<Match when={itemType() === "image"}>
|
||||||
<img
|
<img
|
||||||
|
data-sort={index}
|
||||||
|
data-media-type={item().type}
|
||||||
src={item().previewUrl}
|
src={item().previewUrl}
|
||||||
width={item().meta?.small?.width}
|
width={item().meta?.small?.width}
|
||||||
height={item().meta?.small?.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={itemStyle(item())}
|
||||||
></img>
|
></img>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={itemType() === "video"}>
|
<Match when={itemType() === "video"}>
|
||||||
<video
|
<video
|
||||||
|
data-sort={index}
|
||||||
|
data-media-type={item().type}
|
||||||
src={item().url || undefined}
|
src={item().url || undefined}
|
||||||
autoplay={!props.sensitive && settings().autoPlayVideos}
|
autoplay={settings().autoPlayVideos}
|
||||||
playsinline={settings().autoPlayVideos ? true : undefined}
|
playsinline={settings().autoPlayVideos ? true : undefined}
|
||||||
controls
|
controls
|
||||||
poster={item().previewUrl}
|
poster={item().previewUrl}
|
||||||
width={item().meta?.small?.width}
|
width={item().meta?.small?.width}
|
||||||
height={item().meta?.small?.height}
|
height={item().meta?.small?.height}
|
||||||
|
style={itemStyle(item())}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={itemType() === "gifv"}>
|
<Match when={itemType() === "gifv"}>
|
||||||
<video
|
<video
|
||||||
|
data-sort={index}
|
||||||
|
data-media-type={item().type}
|
||||||
src={item().url || undefined}
|
src={item().url || undefined}
|
||||||
autoplay={!props.sensitive && settings().autoPlayGIFs}
|
autoplay={settings().autoPlayGIFs}
|
||||||
controls
|
controls
|
||||||
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={item().meta?.small?.width}
|
width={item().meta?.small?.width}
|
||||||
height={item().meta?.small?.height}
|
height={item().meta?.small?.height}
|
||||||
|
style={itemStyle(item())}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={itemType() === "audio"}>
|
<Match when={itemType() === "audio"}>
|
||||||
<audio src={item().url || undefined} controls></audio>
|
<audio
|
||||||
|
data-sort={index}
|
||||||
|
data-media-type={item().type}
|
||||||
|
src={item().url || undefined}
|
||||||
|
controls
|
||||||
|
></audio>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Index>
|
</Index>
|
||||||
|
|
|
@ -5,8 +5,6 @@ import {
|
||||||
type JSX,
|
type JSX,
|
||||||
Show,
|
Show,
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
createSignal,
|
|
||||||
type Setter,
|
|
||||||
} 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";
|
||||||
|
@ -33,9 +31,9 @@ import MediaAttachmentGrid from "./MediaAttachmentGrid.js";
|
||||||
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 "./toots/TootContent";
|
import TootContent from "./toot-components/TootContent";
|
||||||
import BoostIcon from "./toots/BoostIcon";
|
import BoostIcon from "./toot-components/BoostIcon";
|
||||||
import PreviewCard from "./toots/PreviewCard";
|
import PreviewCard from "./toot-components/PreviewCard";
|
||||||
|
|
||||||
type TootActionGroupProps<T extends mastodon.v1.Status> = {
|
type TootActionGroupProps<T extends mastodon.v1.Status> = {
|
||||||
onRetoot?: (value: T) => void;
|
onRetoot?: (value: T) => void;
|
||||||
|
@ -47,7 +45,7 @@ type TootActionGroupProps<T extends mastodon.v1.Status> = {
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RegularTootProps = {
|
type TootCardProps = {
|
||||||
status: mastodon.v1.Status;
|
status: mastodon.v1.Status;
|
||||||
actionable?: boolean;
|
actionable?: boolean;
|
||||||
evaluated?: boolean;
|
evaluated?: boolean;
|
||||||
|
@ -202,11 +200,6 @@ export function findElementActionable(
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleReveal(setValue: Setter<boolean>, event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
setValue((x) => !x);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for a toot.
|
* Component for a toot.
|
||||||
*
|
*
|
||||||
|
@ -235,7 +228,7 @@ function onToggleReveal(setValue: Setter<boolean>, event: Event) {
|
||||||
* You can extract the intent from the attributes of the "actionable" element.
|
* You can extract the intent from the attributes of the "actionable" element.
|
||||||
* The action type is the dataset's `action`.
|
* The action type is the dataset's `action`.
|
||||||
*/
|
*/
|
||||||
const RegularToot: Component<RegularTootProps> = (props) => {
|
const RegularToot: Component<TootCardProps> = (props) => {
|
||||||
let rootRef: HTMLElement;
|
let rootRef: HTMLElement;
|
||||||
const [managed, managedActionGroup, rest] = splitProps(
|
const [managed, managedActionGroup, rest] = splitProps(
|
||||||
props,
|
props,
|
||||||
|
@ -246,7 +239,6 @@ const RegularToot: Component<RegularTootProps> = (props) => {
|
||||||
const status = () => managed.status;
|
const status = () => managed.status;
|
||||||
const toot = () => status().reblog ?? status();
|
const toot = () => status().reblog ?? status();
|
||||||
const session = useDefaultSession();
|
const session = useDefaultSession();
|
||||||
const [reveal, setReveal] = createSignal(false);
|
|
||||||
|
|
||||||
css`
|
css`
|
||||||
.reply-sep {
|
.reply-sep {
|
||||||
|
@ -293,7 +285,7 @@ const RegularToot: Component<RegularTootProps> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<article
|
<section
|
||||||
classList={{
|
classList={{
|
||||||
[tootStyle.toot]: true,
|
[tootStyle.toot]: true,
|
||||||
[tootStyle.expanded]: managed.evaluated,
|
[tootStyle.expanded]: managed.evaluated,
|
||||||
|
@ -334,23 +326,12 @@ const RegularToot: Component<RegularTootProps> = (props) => {
|
||||||
emojis={toot().emojis}
|
emojis={toot().emojis}
|
||||||
mentions={toot().mentions}
|
mentions={toot().mentions}
|
||||||
class={cardStyle.cardNoPad}
|
class={cardStyle.cardNoPad}
|
||||||
sensitive={toot().sensitive}
|
|
||||||
spoilerText={toot().spoilerText}
|
|
||||||
reveal={reveal()}
|
|
||||||
onToggleReveal={[onToggleReveal, setReveal]}
|
|
||||||
/>
|
/>
|
||||||
<Show
|
<Show when={toot().card}>
|
||||||
when={
|
|
||||||
toot().card && (!toot().sensitive || (toot().sensitive && reveal()))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PreviewCard src={toot().card!} />
|
<PreviewCard src={toot().card!} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={toot().mediaAttachments.length > 0}>
|
<Show when={toot().mediaAttachments.length > 0}>
|
||||||
<MediaAttachmentGrid
|
<MediaAttachmentGrid attachments={toot().mediaAttachments} />
|
||||||
attachments={toot().mediaAttachments}
|
|
||||||
sensitive={toot().sensitive}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={managed.actionable}>
|
<Show when={managed.actionable}>
|
||||||
<Divider
|
<Divider
|
||||||
|
@ -359,7 +340,7 @@ const RegularToot: Component<RegularTootProps> = (props) => {
|
||||||
/>
|
/>
|
||||||
<TootActionGroup value={toot()} {...managedActionGroup} />
|
<TootActionGroup value={toot()} {...managedActionGroup} />
|
||||||
</Show>
|
</Show>
|
||||||
</article>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
margin-right: var(--card-pad, 0);
|
margin-right: var(--card-pad, 0);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
||||||
> .content {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
& * {
|
& * {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
68
src/timelines/toot-components/TootContent.tsx
Normal file
68
src/timelines/toot-components/TootContent.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import type { mastodon } from "masto";
|
||||||
|
import {
|
||||||
|
splitProps,
|
||||||
|
type Component,
|
||||||
|
type JSX,
|
||||||
|
createRenderEffect,
|
||||||
|
createMemo,
|
||||||
|
} from "solid-js";
|
||||||
|
import { resolveCustomEmoji } from "../../masto/toot.js";
|
||||||
|
import { makeAcctText, useDefaultSession } from "../../masto/clients";
|
||||||
|
import "./TootContent.css";
|
||||||
|
|
||||||
|
function preventDefault(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TootContentProps = {
|
||||||
|
source?: string;
|
||||||
|
emojis?: mastodon.v1.CustomEmoji[];
|
||||||
|
mentions: mastodon.v1.StatusMention[];
|
||||||
|
} & JSX.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
const TootContent: Component<TootContentProps> = (oprops) => {
|
||||||
|
const session = useDefaultSession();
|
||||||
|
const [props, rest] = splitProps(oprops, [
|
||||||
|
"source",
|
||||||
|
"emojis",
|
||||||
|
"mentions",
|
||||||
|
"class",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const clientFinder = createMemo(() =>
|
||||||
|
session() ? makeAcctText(session()!) : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={(ref) => {
|
||||||
|
createRenderEffect(() => {
|
||||||
|
ref.innerHTML = props.source
|
||||||
|
? props.emojis
|
||||||
|
? resolveCustomEmoji(props.source, props.emojis)
|
||||||
|
: props.source
|
||||||
|
: "";
|
||||||
|
});
|
||||||
|
|
||||||
|
createRenderEffect(() => {
|
||||||
|
const finder = clientFinder();
|
||||||
|
for (const mention of props.mentions) {
|
||||||
|
const elements = ref.querySelectorAll<HTMLAnchorElement>(
|
||||||
|
`a[href='${mention.url}']`,
|
||||||
|
);
|
||||||
|
for (const e of elements) {
|
||||||
|
e.onclick = preventDefault;
|
||||||
|
e.dataset.action = "acct";
|
||||||
|
e.dataset.client = finder;
|
||||||
|
e.dataset.acctId = mention.id.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
class={`TootContent ${props.class || ""}`}
|
||||||
|
{...rest}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TootContent;
|
|
@ -1,115 +0,0 @@
|
||||||
import type { mastodon } from "masto";
|
|
||||||
import {
|
|
||||||
splitProps,
|
|
||||||
type Component,
|
|
||||||
type JSX,
|
|
||||||
createRenderEffect,
|
|
||||||
createMemo,
|
|
||||||
Show,
|
|
||||||
} from "solid-js";
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TootContentProps = JSX.HTMLAttributes<HTMLDivElement> & {
|
|
||||||
source?: string;
|
|
||||||
emojis?: mastodon.v1.CustomEmoji[];
|
|
||||||
mentions: mastodon.v1.StatusMention[];
|
|
||||||
sensitive?: boolean;
|
|
||||||
spoilerText?: string;
|
|
||||||
reveal?: boolean;
|
|
||||||
onToggleReveal?: JSX.EventHandlerUnion<HTMLElement, Event>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TootContent: Component<TootContentProps> = (oprops) => {
|
|
||||||
const [t] = createTranslator(
|
|
||||||
(code) =>
|
|
||||||
import(`./i18n/${code}.json`) as Promise<{
|
|
||||||
default: {
|
|
||||||
cw: string;
|
|
||||||
};
|
|
||||||
}>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const session = useDefaultSession();
|
|
||||||
const [props, rest] = splitProps(oprops, [
|
|
||||||
"source",
|
|
||||||
"emojis",
|
|
||||||
"mentions",
|
|
||||||
"class",
|
|
||||||
"sensitive",
|
|
||||||
"spoilerText",
|
|
||||||
"reveal",
|
|
||||||
"onToggleReveal",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const clientFinder = createMemo(() =>
|
|
||||||
session() ? makeAcctText(session()!) : undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldRevealContent = () => {
|
|
||||||
return !props.sensitive || (props.sensitive && props.reveal);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={(ref) => {
|
|
||||||
createRenderEffect(() => {
|
|
||||||
const finder = clientFinder();
|
|
||||||
for (const mention of props.mentions) {
|
|
||||||
const elements = ref.querySelectorAll<HTMLAnchorElement>(
|
|
||||||
`a[href='${mention.url}']`,
|
|
||||||
);
|
|
||||||
for (const e of elements) {
|
|
||||||
e.onclick = preventDefault;
|
|
||||||
e.dataset.action = "acct";
|
|
||||||
e.dataset.client = finder;
|
|
||||||
e.dataset.acctId = mention.id.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
class={`TootContent ${props.class || ""}`}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<Show when={props.sensitive}>
|
|
||||||
<div>
|
|
||||||
<span
|
|
||||||
ref={(ref) => {
|
|
||||||
createRenderEffect(() => {
|
|
||||||
ref.innerHTML = props.spoilerText
|
|
||||||
? props.emojis
|
|
||||||
? resolveCustomEmoji(props.spoilerText, props.emojis)
|
|
||||||
: props.spoilerText
|
|
||||||
: "";
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
></span>
|
|
||||||
<Button onClick={props.onToggleReveal}>{t("cw")}</Button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={shouldRevealContent()}>
|
|
||||||
<div
|
|
||||||
class="content"
|
|
||||||
ref={(ref) =>
|
|
||||||
createRenderEffect(() => {
|
|
||||||
ref.innerHTML = props.source
|
|
||||||
? props.emojis
|
|
||||||
? resolveCustomEmoji(props.source, props.emojis)
|
|
||||||
: props.source
|
|
||||||
: "";
|
|
||||||
})
|
|
||||||
}
|
|
||||||
></div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TootContent;
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"cw": "\"Content Warning\""
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"cw": "“内容警告”"
|
|
||||||
}
|
|
Loading…
Reference in a new issue