interface: added oembed parser

This commit is contained in:
Tyler Brown Cifu Shuster 2020-08-27 20:15:47 -07:00
parent 6e2ce697f9
commit 8729020fd3
21 changed files with 361 additions and 211 deletions

View File

@ -6807,6 +6807,11 @@
"tslib": "^1.10.0"
}
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"node-forge": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
@ -7058,6 +7063,14 @@
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"dev": true
},
"oembed-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/oembed-parser/-/oembed-parser-1.4.1.tgz",
"integrity": "sha512-1KqnfrXF3TiAQhJ9+vv3dEtMhPSVSOT9D9XPqLjEtaQg5liPc3LQ65YjgKHo7Z/YY/kmZ1PDb5gMcOxxCPPdBA==",
"requires": {
"node-fetch": "^2.6.0"
}
},
"omit-deep": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/omit-deep/-/omit-deep-0.3.0.tgz",
@ -7937,6 +7950,14 @@
"xtend": "^4.0.1"
}
},
"react-oembed-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-oembed-container/-/react-oembed-container-1.0.0.tgz",
"integrity": "sha512-YppvCDgxZkn6qgwAIpxRtmMtxaMpau8yQhm8nzmH7yHpDapmHxzakXvQke5qPfmdYyYW4CsKDfVfGoX14NvQkw==",
"requires": {
"prop-types": "^15.6.0"
}
},
"react-router": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",

View File

@ -20,6 +20,7 @@
"moment": "^2.20.1",
"mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0",
"oembed-parser": "^1.4.1",
"prop-types": "^15.7.2",
"react": "^16.5.2",
"react-codemirror2": "^6.0.1",
@ -29,6 +30,7 @@
"react-dom": "^16.8.6",
"react-helmet": "^6.1.0",
"react-markdown": "^4.3.1",
"react-oembed-container": "^1.0.0",
"react-router-dom": "^5.0.0",
"react-window": "^1.8.5",
"remark-disable-tokenizers": "^1.0.24",

View File

@ -1,6 +1,6 @@
import BaseApi from "./base";
import { StoreState } from "../store/type";
import { BackgroundConfig } from "../types/local-update";
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from "../types/local-update";
export default class LocalApi extends BaseApi<StoreState> {
getBaseHash() {
@ -69,6 +69,16 @@ export default class LocalApi extends BaseApi<StoreState> {
});
}
setRemoteContentPolicy(policy: LocalUpdateRemoteContentPolicy) {
this.store.handleEvent({
data: {
local: {
remoteContentPolicy: policy
}
}
});
}
dehydrate() {
this.store.dehydrate();
}

View File

@ -3,7 +3,7 @@ import { StoreState } from '~/store/type';
import { Cage } from '~/types/cage';
import { LocalUpdate, BackgroundConfig } from '~/types/local-update';
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus'>;
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus' | 'remoteContentPolicy'>;
export default class LocalReducer<S extends LocalState> {
rehydrate(state: S) {
@ -18,7 +18,7 @@ export default class LocalReducer<S extends LocalState> {
}
dehydrate(state: S) {
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background']);
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background', 'remoteContentPolicy']);
localStorage.setItem('localReducer', JSON.stringify(json));
}
reduce(json: Cage, state: S) {
@ -31,6 +31,7 @@ export default class LocalReducer<S extends LocalState> {
this.hideAvatars(data, state)
this.hideNicknames(data, state)
this.omniboxShown(data, state);
this.remoteContentPolicy(data, state);
}
}
baseHash(obj: LocalUpdate, state: S) {
@ -70,6 +71,12 @@ export default class LocalReducer<S extends LocalState> {
}
}
remoteContentPolicy(obj: LocalUpdate, state: S) {
if('remoteContentPolicy' in obj) {
state.remoteContentPolicy = obj.remoteContentPolicy;
}
}
hideAvatars(obj: LocalUpdate, state: S) {
if('hideAvatars' in obj) {
state.hideAvatars = obj.hideAvatars;

View File

@ -54,6 +54,12 @@ export default class GlobalStore extends BaseStore<StoreState> {
suspendedFocus: null,
baseHash: null,
background: undefined,
remoteContentPolicy: {
imageShown: true,
audioShown: true,
videoShown: true,
oembedShown: false,
},
hideAvatars: false,
hideNicknames: false,
invites: {},

View File

@ -11,7 +11,7 @@ import { Permissions } from '~/types/permission-update';
import { LaunchState, WeatherState } from '~/types/launch-update';
import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update';
import { ConnectionStatus } from '~/types/connection';
import { BackgroundConfig } from '~/types/local-update';
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update';
export interface StoreState {
// local state
@ -22,6 +22,7 @@ export interface StoreState {
connection: ConnectionStatus;
baseHash: string | null;
background: BackgroundConfig;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
hideAvatars: boolean;
hideNicknames: boolean;
// invite state

View File

@ -1,12 +1,3 @@
export type LocalUpdate =
LocalUpdateSidebarToggle
| LocalUpdateSetDark
| LocalUpdateBaseHash
| LocalUpdateBackgroundConfig
| LocalUpdateHideAvatars
| LocalUpdateHideNicknames
| LocalUpdateSetOmniboxShown;
interface LocalUpdateSidebarToggle {
sidebarToggle: boolean;
}
@ -31,7 +22,16 @@ interface LocalUpdateHideNicknames {
hideNicknames: boolean;
}
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
interface LocalUpdateSetOmniboxShown {
omniboxShown: boolean;
}
export interface LocalUpdateRemoteContentPolicy {
imageShown: boolean;
audioShown: boolean;
videoShown: boolean;
oembedShown: boolean;
}
interface BackgroundConfigUrl {
type: 'url';
@ -43,6 +43,14 @@ interface BackgroundConfigColor {
color: string;
}
interface LocalUpdateSetOmniboxShown {
omniboxShown: boolean;
}
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
export type LocalUpdate =
LocalUpdateSidebarToggle
| LocalUpdateSetDark
| LocalUpdateBaseHash
| LocalUpdateBackgroundConfig
| LocalUpdateHideAvatars
| LocalUpdateHideNicknames
| LocalUpdateSetOmniboxShown
| LocalUpdateRemoteContentPolicy;

View File

@ -89,7 +89,8 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
pendingMessages,
groups,
hideAvatars,
hideNicknames
hideNicknames,
remoteContentPolicy
} = props;
const renderChannelSidebar = (props, station?) => (
@ -271,6 +272,7 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
chatInitialized={chatInitialized}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
remoteContentPolicy={remoteContentPolicy}
{...props}
/>
</Skeleton>

View File

@ -15,6 +15,7 @@ import { Path, Patp } from "~/types/noun";
import GlobalApi from "~/logic/api/global";
import { Association } from "~/types/metadata-update";
import {Group} from "~/types/group-update";
import { LocalUpdateRemoteContentPolicy } from "~/types";
type ChatScreenProps = RouteComponentProps<{
@ -38,6 +39,7 @@ type ChatScreenProps = RouteComponentProps<{
envelopes: Envelope[];
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
};
interface ChatScreenState {
@ -131,6 +133,7 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
api={props.api}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
remoteContentPolicy={props.remoteContentPolicy}
/>
<ChatInput
api={props.api}

View File

@ -17,7 +17,8 @@ export const ChatMessage = (props) => {
contacts,
unreadRef,
hideAvatars,
hideNicknames
hideNicknames,
remoteContentPolicy
} = props;
// Render sigil if previous message is not by the same sender
@ -46,6 +47,7 @@ export const ChatMessage = (props) => {
association={association}
hideNicknames={hideNicknames}
hideAvatars={hideAvatars}
remoteContentPolicy={remoteContentPolicy}
/>
);

View File

@ -187,6 +187,7 @@ export class ChatWindow extends Component {
contacts={props.contacts}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
remoteContentPolicy={props.remoteContentPolicy}
/>
))
}

View File

@ -1,106 +0,0 @@
import React, { Component } from 'react';
import { Button } from '@tlon/indigo-react';
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
const YOUTUBE_REGEX =
new RegExp(
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol
+ /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links
+ /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
);
export default class UrlContent extends Component {
constructor() {
super();
this.state = {
unfold: false,
copied: false
};
this.unfoldEmbed = this.unfoldEmbed.bind(this);
}
unfoldEmbed(id) {
let unfoldState = this.state.unfold;
unfoldState = !unfoldState;
this.setState({ unfold: unfoldState });
this.iframe.setAttribute('src', this.iframe.dataset.src);
}
render() {
const { props } = this;
const content = props.content;
const imgMatch = IMAGE_REGEX.exec(props.content.url);
const ytMatch = YOUTUBE_REGEX.exec(props.content.url);
let contents = content.url;
if (imgMatch) {
contents = (
<img
className="o-80-d"
src={content.url}
style={{
maxWidth: '18rem'
}}
></img>
);
return (
<a className='f7 lh-copy v-top word-break-all'
href={content.url}
target="_blank"
rel="noopener noreferrer"
>
{contents}
</a>
);
} else if (ytMatch) {
contents = (
<div className={'embed-container mb2 w-100 w-75-l w-50-xl ' +
((this.state.unfold === true)
? 'db' : 'dn')}
>
<iframe
ref={(el) => {
this.iframe = el;
}}
width="560"
height="315"
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
frameBorder="0" allow="picture-in-picture, fullscreen"
>
</iframe>
</div>
);
return (
<div>
<a href={content.url}
className='f7 lh-copy v-top bb b--white-d word-break-all'
target="_blank"
rel="noopener noreferrer"
>
{content.url}
</a>
<Button
border={1}
style={{ display: 'inline-flex', height: '1.66em' }} // Height is hacked to line-height until Button supports proper size
ml={1}
onClick={e => this.unfoldEmbed()}
>
{this.state.unfold ? 'collapse' : 'embed'}
</Button>
{contents}
</div>
);
} else {
return (
<a className='f7 lh-copy v-top bb b--white-d b--black word-break-all'
href={content.url}
target="_blank"
rel="noopener noreferrer"
>
{contents}
</a>
);
}
}
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import TextContent from './content/text';
import CodeContent from './content/code';
import UrlContent from './content/url';
import RemoteContent from '~/views/components/RemoteContent';
export default class MessageContent extends Component {
@ -15,7 +15,18 @@ export default class MessageContent extends Component {
if ('code' in content) {
return <CodeContent content={content} />;
} else if ('url' in content) {
return <UrlContent content={content} />;
return (
<RemoteContent
url={content.url}
remoteContentPolicy={props.remoteContentPolicy}
imageProps={{style: {
maxWidth: '18rem'
}}}
videoProps={{style: {
maxWidth: '18rem'
}}}
/>
);
} else if ('me' in content) {
return (
<p className='f7 i lh-copy v-top'>

View File

@ -31,7 +31,7 @@ export const Message = (props) => {
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
<div className="fr f7 clamp-message white-d pr3 lh-copy"
style={{ flexGrow: 1 }}>
<MessageContent letter={props.msg.letter} />
<MessageContent letter={props.msg.letter} remoteContentPolicy={props.remoteContentPolicy}/>
</div>
</div>
)
@ -98,7 +98,7 @@ const renderWithSigil = (props, timestamp) => {
{datestamp}
</p>
</div>
<MessageContent letter={props.msg.letter} />
<MessageContent letter={props.msg.letter} remoteContentPolicy={props.remoteContentPolicy} />
</div>
</div>
);

View File

@ -116,20 +116,8 @@ h2 {
100% {transform: rotate(360deg);}
}
/* embeds */
.embed-container {
position: relative;
height: 0;
overflow: hidden;
padding-bottom: 28.125%;
}
.embed-container iframe, .embed-container object, .embed-container embed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
.embed-container iframe {
max-width: 100%;
}
.mh-16 {
@ -188,9 +176,6 @@ h2 {
.h-100-minus-96-s {
height: calc(100% - 96px);
}
.embed-container {
padding-bottom: 56.25%;
}
.unread-notice {
top: 96px;
}
@ -200,18 +185,12 @@ h2 {
.flex-basis-250-m {
flex-basis: 250px;
}
.embed-container {
padding-bottom: 56.25%;
}
}
@media all and (min-width: 46.875em) and (max-width: 60em) {
.flex-basis-250-l {
flex-basis: 250px;
}
.embed-container {
padding-bottom: 37.5%;
}
}
@media all and (min-width: 60em) {

View File

@ -59,7 +59,7 @@ export class LinksApp extends Component {
props.invites : {};
const listening = props.linkListening;
const { api, sidebarShown, hideAvatars, hideNicknames, s3 } = this.props;
const { api, sidebarShown, hideAvatars, hideNicknames, s3, remoteContentPolicy } = this.props;
return (
<>
@ -319,6 +319,7 @@ export class LinksApp extends Component {
api={api}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
remoteContentPolicy={remoteContentPolicy}
/>
</Skeleton>
);

View File

@ -2,6 +2,8 @@ import React, { Component } from 'react';
import { cite } from '~/logic/lib/util';
import moment from 'moment';
import RemoteContent from "~/views/components/RemoteContent";
export class LinkPreview extends Component {
constructor(props) {
super(props);
@ -27,26 +29,6 @@ export class LinkPreview extends Component {
timeSinceLinkPost: this.getTimeSinceLinkPost()
});
}, 60000);
// check for soundcloud for fetching embed
const soundcloudRegex = new RegExp(String(/(https?:\/\/(?:www.)?soundcloud.com\/[\w-]+\/?(?:sets\/)?[\w-]+)/.source)
);
const isSoundcloud = soundcloudRegex.exec(this.props.url);
if (isSoundcloud && this.state.embed === '') {
fetch(
'https://soundcloud.com/oembed?format=json&url=' +
encodeURIComponent(this.props.url))
.then((response) => {
return response.json();
})
.then((json) => {
this.setState({ embed: json.html });
});
} else if (!isSoundcloud) {
this.setState({ embed: '' });
}
}
componentWillUnmount() {
@ -66,6 +48,16 @@ export class LinkPreview extends Component {
render() {
const { props } = this;
const embed = (
<RemoteContent
unfold={true}
renderUrl={false}
url={props.url}
remoteContentPolicy={props.remoteContentPolicy}
className="mw-100"
/>
);
const URLparser = new RegExp(
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
);
@ -76,42 +68,6 @@ export class LinkPreview extends Component {
hostname = hostname[4];
}
const imgMatch = /(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM)$/.exec(
props.url
);
const youTubeRegex = new RegExp(
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) + // protocol
/(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source + // short and long-links
/(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
);
const ytMatch = youTubeRegex.exec(props.url);
let embed = '';
if (imgMatch) {
embed = <a href={props.url}
target="_blank"
style={{ width: 'max-content' }}
>
<img src={props.url} style={{ maxHeight: '500px', maxWidth: '100%' }} />
</a>;
}
if (ytMatch) {
embed = (
<iframe
ref="iframe"
width="560"
height="315"
src={`https://www.youtube.com/embed/${ytMatch[1]}`}
frameBorder="0"
allow="picture-in-picture, fullscreen"
></iframe>
);
}
const showNickname = props.nickname && !props.hideNicknames;
const nameClass = showNickname ? 'inter' : 'mono';
@ -119,9 +75,9 @@ export class LinkPreview extends Component {
return (
<div className="pb6 w-100">
<div
className={'w-100 tc ' + (ytMatch ? 'links embed-container' : '')}
className={'w-100 tc'}
>
{embed || <div dangerouslySetInnerHTML={{ __html: this.state.embed }} />}
{embed}
</div>
<div className="flex flex-column ml2 pt6 flex-auto">
<a href={props.url} className="w-100 flex" target="_blank" rel="noopener noreferrer">

View File

@ -167,6 +167,7 @@ export class LinkDetail extends Component {
linkIndex={props.linkIndex}
time={this.state.data.time}
hideNicknames={props.hideNicknames}
remoteContentPolicy={props.remoteContentPolicy}
/>
<div className="relative">
<div className={'relative ba br1 mt6 mb6 ' + focus}>

View File

@ -0,0 +1,94 @@
import React from "react";
import { Box, Button, Checkbox } from '@tlon/indigo-react';
import { Formik, Form } from "formik";
import * as Yup from "yup";
import GlobalApi from "~/logic/api/global";
import { LocalUpdateRemoteContentPolicy } from "~/types/local-update";
const formSchema = Yup.object().shape({
imageShown: Yup.boolean(),
audioShown: Yup.boolean(),
videoShown: Yup.boolean(),
oembedShown: Yup.boolean()
});
interface FormSchema {
imageShown: boolean;
audioShown: boolean;
videoShown: boolean;
oembedShown: boolean;
}
interface RemoteContentFormProps {
api: GlobalApi;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
}
export default function RemoteContentForm(props: RemoteContentFormProps) {
const { api, remoteContentPolicy } = props;
const imageShown = remoteContentPolicy.imageShown;
const audioShown = remoteContentPolicy.audioShown;
const videoShown = remoteContentPolicy.videoShown;
const oembedShown = remoteContentPolicy.oembedShown;
return (
<Formik
validationSchema={formSchema}
initialValues={
{
imageShown,
audioShown,
videoShown,
oembedShown
} as FormSchema
}
onSubmit={(values, actions) => {
api.local.setRemoteContentPolicy({
imageShown: values.imageShown,
audioShown: values.audioShown,
videoShown: values.videoShown,
oembedShown: values.oembedShown
});
api.local.dehydrate();
actions.setSubmitting(false);
}}
>
{(props) => (
<Form>
<Box
display="grid"
gridTemplateColumns="1fr"
gridTemplateRows="audio"
gridRowGap={3}
>
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
Remote Content
</Box>
<Box>
<Checkbox
label="Load images"
id="imageShown"
/>
<Checkbox
label="Load audio files"
id="audioShown"
/>
<Checkbox
label="Load video files"
id="videoShown"
/>
<Checkbox
label="Load embedded content"
id="oembedShown"
caption="Embedded content may contain scripts"
/>
</Box>
</Box>
<Button border={1} borderColor="washedGray" type="submit">
Save
</Button>
</Form>
)}
</Formik>
);
}

View File

@ -19,6 +19,7 @@ import { StoreState } from "../../../store/type";
import DisplayForm from "./lib/DisplayForm";
import S3Form from "./lib/S3Form";
import SecuritySettings from "./lib/Security";
import RemoteContentForm from "./lib/RemoteContent";
type ProfileProps = StoreState & { api: GlobalApi; ship: string };
@ -30,6 +31,7 @@ export default function Settings({
hideAvatars,
hideNicknames,
background,
remoteContentPolicy
}: ProfileProps) {
return (
<Box
@ -51,6 +53,7 @@ export default function Settings({
background={background}
s3={s3}
/>
<RemoteContentForm {...{api, remoteContentPolicy}} />
<S3Form api={api} s3={s3} />
<SecuritySettings api={api} />
</Box>

View File

@ -0,0 +1,148 @@
import React, { Component, Fragment } from 'react';
import { LocalUpdateRemoteContentPolicy } from "~/types/local-update";
import { Button } from '@tlon/indigo-react';
import { hasProvider } from 'oembed-parser';
import EmbedContainer from 'react-oembed-container';
interface RemoteContentProps {
url: string;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
unfold: boolean;
renderUrl: boolean;
imageProps: any;
audioProps: any;
videoProps: any;
oembedProps: any;
}
interface RemoteContentState {
unfold: boolean;
embed: any | undefined;
}
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg)$/i);
const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i);
export default class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
constructor(props) {
super(props);
this.state = {
unfold: props.unfold || false,
embed: undefined
};
this.unfoldEmbed = this.unfoldEmbed.bind(this);
this.loadOembed = this.loadOembed.bind(this);
this.wrapInLink = this.wrapInLink.bind(this);
}
unfoldEmbed() {
let unfoldState = this.state.unfold;
unfoldState = !unfoldState;
this.setState({ unfold: unfoldState });
}
loadOembed() {
fetch(`https://noembed.com/embed?url=${this.props.url}`)
.then(response => response.json())
.then((result) => {
this.setState({ embed: result });
}).catch((error) => {
this.setState({ embed: 'error' });
console.log('error fetching oembed', error);
});
}
wrapInLink(contents) {
return (<a
href={this.props.url}
className={`f7 lh-copy v-top word-break-all ${(typeof contents === 'string') ? 'bb b--white-d b--black' : ''}`}
target="_blank"
rel="noopener noreferrer"
>
{contents}
</a>);
}
render() {
const {
remoteContentPolicy,
url,
unfold = false,
renderUrl = true,
imageProps = {},
audioProps = {},
videoProps = {},
oembedProps = {},
...props
} = this.props;
const isImage = IMAGE_REGEX.test(url);
const isAudio = AUDIO_REGEX.test(url);
const isVideo = VIDEO_REGEX.test(url);
const isOembed = hasProvider(url);
if (isImage && remoteContentPolicy.imageShown) {
return this.wrapInLink(
<img
src={url}
{...imageProps}
{...props}
/>
);
} else if (isAudio && remoteContentPolicy.audioShown) {
return (
<>
{renderUrl ? this.wrapInLink(url) : null}
<audio
controls
className="db"
src={url}
{...audioProps}
{...props}
/>
</>
);
} else if (isVideo && remoteContentPolicy.videoShown) {
return (
<>
{renderUrl ? this.wrapInLink(url) : null}
<video
controls
className="db"
src={url}
{...videoProps}
{...props}
/>
</>
);
} else if (isOembed && remoteContentPolicy.oembedShown) {
this.loadOembed();
return (
<Fragment>
{renderUrl ? this.wrapInLink(this.state.embed && this.state.embed.title ? this.state.embed.title : url) : null}
{this.state.embed !== 'error' && !unfold ? <Button
border={1}
style={{ display: 'inline-flex', height: '1.66em' }} // Height is hacked to line-height until Button supports proper size
ml={1}
onClick={this.unfoldEmbed}
>
{this.state.unfold ? 'collapse' : 'expand'}
</Button> : null}
<div
className={'embed-container mb2 w-100 w-75-l w-50-xl ' + (this.state.unfold ? 'db' : 'dn')}
{...oembedProps}
{...props}
>
{this.state.embed && this.state.embed.html && this.state.unfold
? <EmbedContainer markup={this.state.embed.html}>
<div dangerouslySetInnerHTML={{__html: this.state.embed.html}}></div>
</EmbedContainer>
: null}
</div>
</Fragment>
);
} else {
return renderUrl ? this.wrapInLink(url) : null;
}
}
}