Video support (#55)

- Added Vimeo and YouTube link preview support
This commit is contained in:
Will G 2022-05-06 11:06:29 +10:00 committed by GitHub
parent 07c5854649
commit 506f2ffe09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 125 additions and 12 deletions

View File

@ -177,3 +177,7 @@ ul li > p {
.embed-content blockquote > a {
@apply cursor-pointer text-blue hover:underline;
}
.embed-content iframe {
@apply mb-6;
}

View File

@ -1,8 +1,10 @@
import { IEmbed, INoembed, isNoembed } from '@/services/embed';
import { ReactElement, useEffect, useRef } from 'react';
import { ReactElement, useEffect, useRef, useState } from 'react';
import { Button } from './Button';
import Image from 'next/image';
import Link from 'next/link';
import { TOS } from '@/constants';
import classNames from 'classnames';
interface Props {
@ -13,15 +15,80 @@ interface Props {
export default function EmbedContent(props: Props): ReactElement {
const { content, classes } = props;
const htmlRef = useRef<HTMLDivElement>(null);
const [allowExternalContent, setAllowExternalContent] = useState(false);
useEffect(() => {
if (isNoembed(content) && null !== htmlRef.current) {
htmlRef.current.innerHTML = content.html;
}
}, [content]);
if (allowExternalContent) {
if (isNoembed(content) && null !== htmlRef.current) {
htmlRef.current.innerHTML = content.html;
}
}
}, [allowExternalContent, content]);
if (isNoembed(content)) {
return (
<div className={classNames('embed-content', classes)} ref={htmlRef}></div>
);
if (content.isExternalVideo) {
return allowExternalContent ? (
<div
className={classNames('embed-content', classes)}
ref={htmlRef}
></div>
) : (
<div
className={classNames(
'embed-content w-full border border-primary my-6 mx-auto max-w-sm p-6',
)}
>
<p
className={classNames(
'text-sm font-semibold mb-2 leading-relaxed',
'tablet:text-base',
)}
>
This content is hosted by {content.site_name}.
</p>
<p
className={classNames(
'text-xs font-normal leading-relaxed mb-4',
'tablet:text-sm',
)}
>
By showing the external content you accept their{' '}
{TOS[content.site_name] && TOS[content.site_name].length > 0 ? (
<a
href={TOS[content.site_name]}
target="_blank"
rel="noreferrer"
className={classNames('text-primary font-semibold')}
>
Terms and Conditions
</a>
) : (
'Terms and Conditions'
)}
.
</p>
<Button
size="large"
onClick={() => {
setAllowExternalContent(true);
}}
className={'block ml-auto'}
>
Show
</Button>
</div>
);
} else {
return (
<div
className={classNames('embed-content', classes)}
ref={htmlRef}
></div>
);
}
} else {
return (
<Link href={content.url}>

View File

@ -2,6 +2,7 @@ import CMS from '@/constants/cms';
import METADATA from '@/constants/metadata';
import NAVIGATION from '@/constants/navigation';
import SEARCH from '@/constants/search';
import TOS from './tos';
import UI from '@/constants/ui';
export { UI, CMS, NAVIGATION, METADATA, SEARCH };
export { CMS, METADATA, NAVIGATION, SEARCH, TOS, UI };

6
constants/tos.ts Normal file
View File

@ -0,0 +1,6 @@
const TOS: Record<string, string> = {
Vimeo: 'https://vimeo.com/terms',
YouTube: 'https://www.youtube.com/static?template=terms',
};
export default TOS;

View File

@ -7,10 +7,10 @@ const ContentSecurityPolicy = `
script-src 'self' ${
process.env.NODE_ENV == 'development' ? "'unsafe-eval' " : ''
}'unsafe-inline' *.ctfassets.net *.youtube.com *.twitter.com;
child-src 'self' *.ctfassets.net *.youtube.com *.twitter.com;
child-src 'self' *.ctfassets.net *.youtube.com player.vimeo.com *.twitter.com;
style-src 'self' 'unsafe-inline' *.googleapis.com;
img-src 'self' blob: data: *.ctfassets.net *.youtube.com *.twitter.com;
media-src 'self';
media-src 'self' *.youtube.com;
connect-src *;
font-src 'self' blob: data: fonts.gstatic.com maxcdn.bootstrapcdn.com;
worker-src 'self' blob:;

View File

@ -7,10 +7,10 @@ export interface IEmbed {
description?: string;
site_name?: string;
image?: string;
isExternalVideo?: boolean;
}
function extractMetadata(html: string): IEmbed {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const himalaya = require('himalaya');
html = html.trim();
const nodes = himalaya.parse(html);
@ -95,7 +95,9 @@ export function isNoembed(object: unknown): object is INoembed {
export async function fetchContent(
targetUrl: string,
): Promise<IEmbed | INoembed> {
const fetchUrl = `https://noembed.com/embed?url=${targetUrl}`;
const fetchUrl = `https://noembed.com/embed?url=${encodeURIComponent(
targetUrl,
)}`;
const response = await fetch(fetchUrl);
let data = await response.json();
@ -118,10 +120,43 @@ export async function fetchContent(
}
function convertToNoembed(rawData: any): INoembed {
return {
const noembed: INoembed = {
title: sanitize(rawData.title),
url: sanitize(rawData.url),
site_name: sanitize(rawData.provider_name),
html: sanitize(rawData.html),
html: '',
};
switch (noembed.site_name) {
case 'Vimeo':
case 'YouTube':
const himalaya = require('himalaya');
let nodes = himalaya.parse(rawData.html);
nodes[0].attributes = nodes[0].attributes.map((attr: any) => {
switch (attr.key) {
case 'width':
attr.value = '100%';
break;
case 'height':
// Vimeo player has a fixed height
if (noembed.site_name !== 'Vimeo') {
attr.value = '350px';
}
break;
default:
break;
}
return attr;
});
noembed.html = himalaya.stringify(nodes);
noembed.isExternalVideo = true;
break;
default:
noembed.html = sanitize(rawData.html);
break;
}
return noembed;
}