feat: support initialHidden in PreviewedLink

This commit is contained in:
Shusui MOYATANI
2024-01-08 01:38:53 +09:00
parent ce125bc643
commit 805d401632
5 changed files with 82 additions and 40 deletions

View File

@@ -1,14 +1,15 @@
import { Component, JSX, Switch, Match, createEffect, Show } from 'solid-js'; import { Component, JSX, Switch, Match, createSignal, createEffect, Show } from 'solid-js';
import LazyLoad from '@/components/utils/LazyLoad'; import LazyLoad from '@/components/utils/LazyLoad';
import SafeLink from '@/components/utils/SafeLink'; import SafeLink from '@/components/utils/SafeLink';
import useConfig from '@/core/useConfig'; import useConfig from '@/core/useConfig';
import { useOgp } from '@/utils/ogp'; import { useTranslation } from '@/i18n/useTranslation';
import { useOgp, isOgpUrl } from '@/utils/ogp';
import { isTwitterUrl, parseYouTubeVideoUrl } from '@/utils/url'; import { isTwitterUrl, parseYouTubeVideoUrl } from '@/utils/url';
type PreviewdLinkProps = { type PreviewdLinkProps = {
class?: string; url: string;
href: string; initialHidden: boolean;
children?: JSX.Element; children?: JSX.Element;
}; };
@@ -29,13 +30,13 @@ const youtubeUrl = (videoId: string): string => {
return iframeUrl.href; return iframeUrl.href;
}; };
const TwitterEmbed: Component<{ class?: string; href: string }> = (props) => { const TwitterEmbed: Component<{ url: string }> = (props) => {
let twitterRef: HTMLQuoteElement | undefined; let twitterRef: HTMLQuoteElement | undefined;
const { getColorTheme } = useConfig(); const { getColorTheme } = useConfig();
createEffect(() => { createEffect(() => {
if (isTwitterUrl(props.href)) { if (isTwitterUrl(props.url)) {
window.twttr?.widgets?.load(twitterRef); window.twttr?.widgets?.load(twitterRef);
} }
}); });
@@ -51,12 +52,12 @@ const TwitterEmbed: Component<{ class?: string; href: string }> = (props) => {
return ( return (
<blockquote ref={twitterRef} class="twitter-tweet" data-theme={dataTheme()}> <blockquote ref={twitterRef} class="twitter-tweet" data-theme={dataTheme()}>
<a <a
class={props.class} class="text-link underline"
href={twitterUrl(props.href)} href={twitterUrl(props.url)}
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
{twitterUrl(props.href)} {twitterUrl(props.url)}
</a> </a>
</blockquote> </blockquote>
); );
@@ -68,7 +69,7 @@ const OgpEmbed: Component<{ class?: string; url: string }> = (props) => {
})); }));
return ( return (
<Show when={ogp()} fallback={<SafeLink class={props.class} href={props.url} />} keyed> <Show when={ogp()} fallback={<SafeLink class="text-link underline" href={props.url} />} keyed>
{(ogpProps) => ( {(ogpProps) => (
<SafeLink href={props.url}> <SafeLink href={props.url}>
<div class="my-2 rounded-lg border border-border transition-colors hover:bg-bg-tertiary"> <div class="my-2 rounded-lg border border-border transition-colors hover:bg-bg-tertiary">
@@ -89,20 +90,53 @@ const OgpEmbed: Component<{ class?: string; url: string }> = (props) => {
); );
}; };
type ClickToShowProps = {
initialHidden: boolean;
url: string;
children: JSX.Element;
};
const ClickToShow: Component<ClickToShowProps> = (props) => {
const i18n = useTranslation();
const [hidden, setHidden] = createSignal(props.initialHidden);
return (
<Show
when={!hidden()}
fallback={
<div>
<button
class="flex flex-col items-center rounded bg-bg-tertiary p-3 text-xs text-fg-secondary hover:shadow"
onClick={() => setHidden(false)}
>
{i18n()('post.showPreview')}
</button>
<SafeLink class="text-link underline" href={props.url} />
</div>
}
>
{props.children}
</Show>
);
};
const PreviewedLink: Component<PreviewdLinkProps> = (props) => { const PreviewedLink: Component<PreviewdLinkProps> = (props) => {
const { config } = useConfig(); const { config } = useConfig();
return ( return (
<Switch fallback={<SafeLink class={props.class} href={props.href} />}> <Switch fallback={<SafeLink class="text-link underline" href={props.url} />}>
<Match when={config().embedding.twitter && isTwitterUrl(props.href)}> <Match when={config().embedding.twitter && isTwitterUrl(props.url)}>
<TwitterEmbed class={props.class} href={props.href} /> <ClickToShow url={props.url} initialHidden={props.initialHidden}>
<TwitterEmbed url={props.url} />
</ClickToShow>
</Match> </Match>
<Match when={config().embedding.youtube && parseYouTubeVideoUrl(props.href)} keyed> <Match when={config().embedding.youtube && parseYouTubeVideoUrl(props.url)} keyed>
{({ videoId }) => ( {({ videoId }) => (
<ClickToShow url={props.url} initialHidden={props.initialHidden}>
<LazyLoad <LazyLoad
fallback={ fallback={
<div class="aspect-video max-w-full"> <div class="aspect-video max-w-full">
<SafeLink href={props.href} /> <SafeLink href={props.url} />
</div> </div>
} }
> >
@@ -118,10 +152,13 @@ const PreviewedLink: Component<PreviewdLinkProps> = (props) => {
</div> </div>
)} )}
</LazyLoad> </LazyLoad>
</ClickToShow>
)} )}
</Match> </Match>
<Match when={config().embedding.ogp}> <Match when={config().embedding.ogp && isOgpUrl(props.url)}>
<LazyLoad>{() => <OgpEmbed class={props.class} url={props.href} />}</LazyLoad> <ClickToShow url={props.url} initialHidden={props.initialHidden}>
<LazyLoad>{() => <OgpEmbed url={props.url} />}</LazyLoad>
</ClickToShow>
</Match> </Match>
</Switch> </Switch>
); );

View File

@@ -64,7 +64,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
</button> </button>
); );
} }
return <PreviewedLink class="text-link underline" href={item.content} />; return <PreviewedLink url={item.content} initialHidden={initialHidden()} />;
} }
if (item.type === 'TagReferenceResolved') { if (item.type === 'TagReferenceResolved') {
if (item.reference == null) { if (item.reference == null) {

View File

@@ -108,6 +108,7 @@ export default {
failedToDelete: 'Failed to delete', failedToDelete: 'Failed to delete',
showImage: 'Show image', showImage: 'Show image',
showVideo: 'Show video', showVideo: 'Show video',
showPreview: 'Show preview',
showOverflow: 'Read more', showOverflow: 'Read more',
hideOverflow: 'Hide', hideOverflow: 'Hide',
download: 'Download', download: 'Download',

View File

@@ -104,6 +104,7 @@ export default {
failedToDelete: 'すべてのリレーで削除に失敗しました', failedToDelete: 'すべてのリレーで削除に失敗しました',
showImage: '画像を表示する', showImage: '画像を表示する',
showVideo: '動画を表示する', showVideo: '動画を表示する',
showPreview: 'プレビューを表示する',
showOverflow: '続きを読む', showOverflow: '続きを読む',
hideOverflow: '隠す', hideOverflow: '隠す',
download: 'ダウンロード', download: 'ダウンロード',

View File

@@ -39,13 +39,16 @@ export const parseOgp = (text: string): OgpContent | null => {
return parseOgpFromDOM(doc); return parseOgpFromDOM(doc);
}; };
export const fetchOgpContent = async (urlString: string): Promise<OgpContent | null> => { export const isOgpUrl = (urlString: string): boolean => {
const allowList = ['www3.nhk.or.jp']; const allowList = ['www3.nhk.or.jp'];
const url = new URL(urlString); const url = new URL(urlString);
if (!allowList.includes(url.host)) return null; return allowList.includes(url.host);
};
const res = await fetch(url, { headers: { Accept: 'text/html' } }); export const fetchOgpContent = async (urlString: string): Promise<OgpContent | null> => {
if (!isOgpUrl(urlString)) return null;
const res = await fetch(urlString, { headers: { Accept: 'text/html' } });
const text = await res.text(); const text = await res.text();
return parseOgp(text); return parseOgp(text);
}; };