fc32339d2d
When Safari parses lbry:// URLs, it breaks them as they are not compliant with the RFC 3986 URI syntax. This causes iframes in text posts which link to a lbry:// URL to not display on Safari. Instead, just use the lbry:// URL matched from the iframe regex instead of parsing the iframe on Safari.
283 lines
8.2 KiB
JavaScript
283 lines
8.2 KiB
JavaScript
// @flow
|
|
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS, MISSING_THUMB_DEFAULT } from 'config';
|
|
import { platform } from 'util/platform';
|
|
import { formattedEmote, inlineEmote } from 'util/remark-emote';
|
|
import { formattedLinks, inlineLinks } from 'util/remark-lbry';
|
|
import { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp';
|
|
import { getThumbnailCdnUrl, getImageProxyUrl } from 'util/thumbnail';
|
|
import * as ICONS from 'constants/icons';
|
|
import * as React from 'react';
|
|
import Button from 'component/button';
|
|
import classnames from 'classnames';
|
|
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
|
import MarkdownLink from 'component/markdownLink';
|
|
import OptimizedImage from 'component/optimizedImage';
|
|
import reactRenderer from 'remark-react';
|
|
import remark from 'remark';
|
|
import remarkAttr from 'remark-attr';
|
|
import remarkBreaks from 'remark-breaks';
|
|
import remarkEmoji from 'remark-emoji';
|
|
import remarkFrontMatter from 'remark-frontmatter';
|
|
import remarkStrip from 'strip-markdown';
|
|
import ZoomableImage from 'component/zoomableImage';
|
|
|
|
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
|
|
|
|
function isEmote(title, src) {
|
|
return title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons');
|
|
}
|
|
|
|
function isStakeEnoughForPreview(stakedLevel) {
|
|
return !stakedLevel || stakedLevel >= CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS;
|
|
}
|
|
|
|
type SimpleTextProps = {
|
|
children?: React.Node,
|
|
};
|
|
|
|
type SimpleLinkProps = {
|
|
href?: string,
|
|
title?: string,
|
|
embed?: boolean,
|
|
children?: React.Node,
|
|
};
|
|
|
|
type ImageLinkProps = {
|
|
src: string,
|
|
title?: string,
|
|
alt?: string,
|
|
helpText?: string,
|
|
};
|
|
|
|
type MarkdownProps = {
|
|
strip?: boolean,
|
|
content: ?string,
|
|
simpleLinks?: boolean,
|
|
noDataStore?: boolean,
|
|
className?: string,
|
|
parentCommentId?: string,
|
|
isMarkdownPost?: boolean,
|
|
disableTimestamps?: boolean,
|
|
stakedLevel?: number,
|
|
setUserMention?: (boolean) => void,
|
|
hasMembership?: boolean,
|
|
};
|
|
|
|
// ****************************************************************************
|
|
// ****************************************************************************
|
|
|
|
const SimpleText = (props: SimpleTextProps) => {
|
|
return <span>{props.children}</span>;
|
|
};
|
|
|
|
// ****************************************************************************
|
|
// ****************************************************************************
|
|
|
|
const SimpleLink = (props: SimpleLinkProps) => {
|
|
const { title, children, href, embed } = props;
|
|
|
|
if (!href) {
|
|
return children || null;
|
|
}
|
|
|
|
if (!href.startsWith('lbry:/')) {
|
|
return (
|
|
<a href={href} title={title} target={'_blank'} rel={'noreferrer noopener'}>
|
|
{children}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
const [uri, search] = href.split('?');
|
|
const urlParams = new URLSearchParams(search);
|
|
const embedParam = urlParams.get('embed');
|
|
|
|
if (embed || embedParam) {
|
|
// Decode this since users might just copy it from the url bar
|
|
const decodedUri = decodeURI(uri);
|
|
return (
|
|
<div className="embed__inline-button embed__inline-button--preview">
|
|
<pre>{decodedUri}</pre>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Dummy link (no 'href')
|
|
return <a title={title}>{children}</a>;
|
|
};
|
|
|
|
// ****************************************************************************
|
|
// ****************************************************************************
|
|
|
|
const SimpleImageLink = (props: ImageLinkProps) => {
|
|
const { src, title, alt, helpText } = props;
|
|
|
|
if (!src) {
|
|
return null;
|
|
}
|
|
|
|
if (isEmote(title, src)) {
|
|
return <OptimizedImage src={src} title={title} className="emote" waitLoad loading="lazy" />;
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
button="link"
|
|
iconRight={ICONS.EXTERNAL}
|
|
label={title || alt || src}
|
|
title={helpText || title || alt || src}
|
|
className="button--external-link"
|
|
href={src}
|
|
/>
|
|
);
|
|
};
|
|
|
|
// ****************************************************************************
|
|
// ****************************************************************************
|
|
|
|
// Use github sanitation schema
|
|
const schema = { ...defaultSchema };
|
|
|
|
// Extend sanitation schema to support lbry protocol
|
|
schema.protocols.href.push('lbry');
|
|
schema.attributes.a.push('embed');
|
|
|
|
const REPLACE_REGEX = /(?:<iframe\s+src=["'])(.*?(?=))(?:["']\s*><\/iframe>)/g;
|
|
|
|
// ****************************************************************************
|
|
// ****************************************************************************
|
|
|
|
export default React.memo<MarkdownProps>(function MarkdownPreview(props: MarkdownProps) {
|
|
const {
|
|
content,
|
|
strip,
|
|
simpleLinks,
|
|
noDataStore,
|
|
className,
|
|
parentCommentId,
|
|
isMarkdownPost,
|
|
disableTimestamps,
|
|
stakedLevel,
|
|
setUserMention,
|
|
hasMembership,
|
|
} = props;
|
|
|
|
const strippedContent = content
|
|
? content.replace(REPLACE_REGEX, (iframeHtml, iframeUrl) => {
|
|
if (platform.isSafari()) {
|
|
return iframeUrl;
|
|
}
|
|
|
|
// Let the browser try to create an iframe to see if the markup is valid
|
|
const outer = document.createElement('div');
|
|
outer.innerHTML = iframeHtml;
|
|
const iframe = ((outer.querySelector('iframe'): any): ?HTMLIFrameElement);
|
|
|
|
if (iframe) {
|
|
const src = iframe.src;
|
|
|
|
if (src && src.startsWith('lbry://')) {
|
|
return src;
|
|
}
|
|
}
|
|
|
|
return iframeHtml;
|
|
})
|
|
: '';
|
|
|
|
const remarkOptions: Object = {
|
|
sanitize: schema,
|
|
fragment: React.Fragment,
|
|
remarkReactComponents: {
|
|
a: noDataStore
|
|
? SimpleLink
|
|
: (linkProps) => (
|
|
<MarkdownLink
|
|
{...linkProps}
|
|
parentCommentId={parentCommentId}
|
|
isMarkdownPost={isMarkdownPost}
|
|
simpleLinks={simpleLinks}
|
|
allowPreview={isStakeEnoughForPreview(stakedLevel) || hasMembership}
|
|
setUserMention={setUserMention}
|
|
/>
|
|
),
|
|
// Workaraund of remarkOptions.Fragment
|
|
div: React.Fragment,
|
|
img: (imgProps) => {
|
|
const isGif = imgProps.src && imgProps.src.endsWith('gif');
|
|
|
|
const imageCdnUrl =
|
|
(isGif
|
|
? getImageProxyUrl(imgProps.src)
|
|
: getThumbnailCdnUrl({ thumbnail: imgProps.src, width: 0, height: 0, quality: 85 })) ||
|
|
MISSING_THUMB_DEFAULT;
|
|
if (noDataStore) {
|
|
return (
|
|
<div className="file-viewer file-viewer--document">
|
|
<img {...imgProps} src={imageCdnUrl} />
|
|
</div>
|
|
);
|
|
} else if ((isStakeEnoughForPreview(stakedLevel) || hasMembership) && !isEmote(imgProps.title, imgProps.src)) {
|
|
return <ZoomableImage {...imgProps} src={imageCdnUrl} />;
|
|
} else {
|
|
return (
|
|
<SimpleImageLink
|
|
src={imageCdnUrl}
|
|
alt={imgProps.alt}
|
|
title={imgProps.title}
|
|
helpText={__('Odysee Premium required to enable image previews')}
|
|
/>
|
|
);
|
|
}
|
|
},
|
|
},
|
|
};
|
|
|
|
const remarkAttrOpts = {
|
|
scope: 'extended',
|
|
elements: ['link'],
|
|
extend: { link: ['embed'] },
|
|
defaultValue: true,
|
|
};
|
|
|
|
// Strip all content and just render text
|
|
if (strip) {
|
|
// Remove new lines and extra space
|
|
remarkOptions.remarkReactComponents.p = SimpleText;
|
|
return (
|
|
<span dir="auto" className="markdown-preview">
|
|
{
|
|
remark()
|
|
.use(remarkStrip)
|
|
.use(remarkFrontMatter, ['yaml'])
|
|
.use(reactRenderer, remarkOptions)
|
|
.processSync(content).contents
|
|
}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div dir="auto" className={classnames('notranslate markdown-preview', className)}>
|
|
{
|
|
remark()
|
|
.use(remarkAttr, remarkAttrOpts)
|
|
// Remark plugins for lbry urls
|
|
// Note: The order is important
|
|
.use(formattedLinks)
|
|
.use(inlineLinks)
|
|
.use(disableTimestamps || isMarkdownPost ? null : inlineTimestamp)
|
|
.use(disableTimestamps || isMarkdownPost ? null : formattedTimestamp)
|
|
// Emojis
|
|
.use(inlineEmote)
|
|
.use(formattedEmote)
|
|
.use(remarkEmoji)
|
|
// Render new lines without needing spaces.
|
|
.use(remarkBreaks)
|
|
.use(remarkFrontMatter, ['yaml'])
|
|
.use(reactRenderer, remarkOptions)
|
|
.processSync(strippedContent).contents
|
|
}
|
|
</div>
|
|
);
|
|
});
|