mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 22:14:26 +01:00
feat: support initialHidden in PreviewedLink
This commit is contained in:
@@ -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,39 +90,75 @@ 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 }) => (
|
||||||
<LazyLoad
|
<ClickToShow url={props.url} initialHidden={props.initialHidden}>
|
||||||
fallback={
|
<LazyLoad
|
||||||
<div class="aspect-video max-w-full">
|
fallback={
|
||||||
<SafeLink href={props.href} />
|
<div class="aspect-video max-w-full">
|
||||||
</div>
|
<SafeLink href={props.url} />
|
||||||
}
|
</div>
|
||||||
>
|
}
|
||||||
{() => (
|
>
|
||||||
<div class="my-2 aspect-video w-full">
|
{() => (
|
||||||
<iframe
|
<div class="my-2 aspect-video w-full">
|
||||||
loading="lazy"
|
<iframe
|
||||||
title="YouTube"
|
loading="lazy"
|
||||||
class="my-2 h-full w-full"
|
title="YouTube"
|
||||||
src={youtubeUrl(videoId)}
|
class="my-2 h-full w-full"
|
||||||
allowfullscreen
|
src={youtubeUrl(videoId)}
|
||||||
/>
|
allowfullscreen
|
||||||
</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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export default {
|
|||||||
failedToDelete: 'すべてのリレーで削除に失敗しました',
|
failedToDelete: 'すべてのリレーで削除に失敗しました',
|
||||||
showImage: '画像を表示する',
|
showImage: '画像を表示する',
|
||||||
showVideo: '動画を表示する',
|
showVideo: '動画を表示する',
|
||||||
|
showPreview: 'プレビューを表示する',
|
||||||
showOverflow: '続きを読む',
|
showOverflow: '続きを読む',
|
||||||
hideOverflow: '隠す',
|
hideOverflow: '隠す',
|
||||||
download: 'ダウンロード',
|
download: 'ダウンロード',
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user