diff --git a/.eslintrc.js b/.eslintrc.js index 312b896..5e0d9e7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -88,6 +88,7 @@ module.exports = { 'notification-icon', 'notification-user', 'notification-event', + 'twitter-tweet', ], }, }, diff --git a/index.html b/index.html index cf0adf2..85d62be 100644 --- a/index.html +++ b/index.html @@ -28,5 +28,6 @@
+ diff --git a/src/components/event/textNote/TextNoteContentDisplay.tsx b/src/components/event/textNote/TextNoteContentDisplay.tsx index 49f4be9..6f970ce 100644 --- a/src/components/event/textNote/TextNoteContentDisplay.tsx +++ b/src/components/event/textNote/TextNoteContentDisplay.tsx @@ -11,6 +11,7 @@ import MentionedUserDisplay from '@/components/event/textNote/MentionedUserDispl import VideoDisplay from '@/components/event/textNote/VideoDisplay'; import EventLink from '@/components/EventLink'; import SafeLink from '@/components/utils/SafeLink'; +import PreviewedLink from '@/components/utils/PreviewedLink'; import { createSearchColumn } from '@/core/column'; import useConfig from '@/core/useConfig'; import { useRequestCommand } from '@/hooks/useCommandBus'; @@ -51,7 +52,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => { if (isVideoUrl(item.content)) { return ; } - return ; + return ; } if (item.type === 'TagReference') { const resolved = event().resolveTagReference(item); diff --git a/src/components/utils/PreviewedLink.tsx b/src/components/utils/PreviewedLink.tsx new file mode 100644 index 0000000..f23b456 --- /dev/null +++ b/src/components/utils/PreviewedLink.tsx @@ -0,0 +1,150 @@ +import { Component, Show, JSX, createSignal, Switch, Match, createEffect, onMount } from 'solid-js'; + +import SafeLink from '@/components/utils/SafeLink'; +import { isTwitterUrl, isYoutubeVideoUrl } from '@/utils/url'; + +type SafeLinkProps = { + class?: string; + href: string; + children?: JSX.Element; +}; + +type OgpContents = { + url: URL | null; + title: string; + description: string; + image: URL | null; +}; + +const twitterUrl = (urlString: string): string => { + try { + const url = new URL(urlString); + url.host = 'twitter.com'; + return url.href; + } catch { + return ''; + } +}; + +const youtubeUrl = (urlString: string): string => { + try { + const originalUrl = new URL(urlString); + const iframeUrl = new URL('https://www.youtube.com/embed'); + if (originalUrl.host === 'youtu.be') { + iframeUrl.pathname += originalUrl.pathname; + } else { + iframeUrl.pathname += `/${originalUrl.searchParams.get('v')}`; + } + iframeUrl.searchParams.set('origin', window.location.origin); + return iframeUrl.href; + } catch { + return ''; + } +}; + +const fetchOgpContents = async (url: URL): Promise => { + const whiteList = ['www3.nhk.or.jp']; + if (whiteList.includes(url.host)) { + const res = await fetch(url, { headers: { Accept: 'text/html' } }); + const text = await res.text(); + const el = new DOMParser().parseFromString(text, 'text/html'); + const ogs: { [index: string]: string } = {}; + Array.from(el.head.querySelectorAll('meta')) + .filter((v) => v.getAttribute('property') != null && v.getAttribute('content') != null) + .forEach((v) => { + const property = v.getAttribute('property'); + const content = v.getAttribute('content'); + if (property != null && content != null) { + ogs[property] = content; + } + }); + + if (ogs['og:image'] && ogs['og:title'] && ogs['og:description']) { + const contents: OgpContents = { + title: ogs['og:title'], + description: ogs['og:description'], + image: new URL(ogs['og:image']), + url, + }; + return contents; + } + } + return null; +}; + +const PreviewedLink: Component = (props) => { + let twitterRef: HTMLQuoteElement | undefined; + + const initialOgp: OgpContents = { title: '', description: '', image: null, url: null }; + const [getOgpContents, setOgpContents] = createSignal(initialOgp); + + const isSafe = () => { + try { + const url = new URL(props.href); + return url.protocol === 'https:' || url.protocol === 'http:'; + } catch { + return false; + } + }; + + const updateOgpContents = async () => { + const ogp = await fetchOgpContents(new URL(props.href)); + if (ogp != null) { + setOgpContents(ogp); + } + }; + + createEffect(() => { + if (isTwitterUrl(props.href)) { + window.twttr?.widgets?.load(twitterRef); + } + }); + + onMount(() => { + updateOgpContents() + .then(() => {}) + .catch(() => {}); + }); + + return ( + + }> + + + + +
+