2020-04-29 22:50:06 +02:00
|
|
|
// @flow
|
2021-11-08 15:08:22 +01:00
|
|
|
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS, SIMPLE_SITE } from 'config';
|
|
|
|
import { formattedEmote, inlineEmote } from 'util/remark-emote';
|
|
|
|
import { formattedLinks, inlineLinks } from 'util/remark-lbry';
|
|
|
|
import { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp';
|
|
|
|
import * as ICONS from 'constants/icons';
|
2020-04-29 22:50:06 +02:00
|
|
|
import * as React from 'react';
|
2021-11-08 15:08:22 +01:00
|
|
|
import Button from 'component/button';
|
2020-09-17 17:32:31 +02:00
|
|
|
import classnames from 'classnames';
|
2021-11-08 15:08:22 +01:00
|
|
|
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
|
|
|
import MarkdownLink from 'component/markdownLink';
|
|
|
|
import OptimizedImage from 'component/optimizedImage';
|
|
|
|
import reactRenderer from 'remark-react';
|
2020-04-29 22:50:06 +02:00
|
|
|
import remark from 'remark';
|
|
|
|
import remarkAttr from 'remark-attr';
|
2020-05-10 06:36:33 +02:00
|
|
|
import remarkBreaks from 'remark-breaks';
|
2021-11-08 15:08:22 +01:00
|
|
|
import remarkEmoji from 'remark-emoji';
|
2020-12-17 23:44:16 +01:00
|
|
|
import remarkFrontMatter from 'remark-frontmatter';
|
2021-11-08 15:08:22 +01:00
|
|
|
import remarkStrip from 'strip-markdown';
|
2021-01-27 04:24:14 +01:00
|
|
|
import ZoomableImage from 'component/zoomableImage';
|
2021-10-27 20:20:47 +02:00
|
|
|
|
|
|
|
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
|
2019-11-07 20:39:22 +01:00
|
|
|
|
2021-11-05 20:31:51 +01:00
|
|
|
function isEmote(title, src) {
|
|
|
|
return title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons');
|
|
|
|
}
|
|
|
|
|
2021-11-08 15:08:22 +01:00
|
|
|
function isStakeEnoughForPreview(stakedLevel) {
|
|
|
|
return !stakedLevel || stakedLevel >= CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS;
|
|
|
|
}
|
|
|
|
|
2020-04-29 22:50:06 +02:00
|
|
|
type SimpleTextProps = {
|
|
|
|
children?: React.Node,
|
|
|
|
};
|
|
|
|
|
|
|
|
type SimpleLinkProps = {
|
|
|
|
href?: string,
|
|
|
|
title?: string,
|
|
|
|
children?: React.Node,
|
|
|
|
};
|
|
|
|
|
2021-04-02 09:57:37 +02:00
|
|
|
type ImageLinkProps = {
|
|
|
|
src: string,
|
|
|
|
title?: string,
|
|
|
|
alt?: string,
|
|
|
|
helpText?: string,
|
|
|
|
};
|
|
|
|
|
2020-04-29 22:50:06 +02:00
|
|
|
type MarkdownProps = {
|
|
|
|
strip?: boolean,
|
|
|
|
content: ?string,
|
2020-10-22 20:16:42 +02:00
|
|
|
simpleLinks?: boolean,
|
2020-09-29 05:46:18 +02:00
|
|
|
noDataStore?: boolean,
|
2020-09-17 17:32:31 +02:00
|
|
|
className?: string,
|
2020-10-20 19:10:02 +02:00
|
|
|
parentCommentId?: string,
|
|
|
|
isMarkdownPost?: boolean,
|
2021-10-08 11:58:17 +02:00
|
|
|
disableTimestamps?: boolean,
|
2021-03-09 10:33:42 +01:00
|
|
|
stakedLevel?: number,
|
2020-04-29 22:50:06 +02:00
|
|
|
};
|
|
|
|
|
2021-01-27 04:24:14 +01:00
|
|
|
// ****************************************************************************
|
|
|
|
// ****************************************************************************
|
|
|
|
|
2020-04-29 22:50:06 +02:00
|
|
|
const SimpleText = (props: SimpleTextProps) => {
|
|
|
|
return <span>{props.children}</span>;
|
|
|
|
};
|
|
|
|
|
2021-01-27 04:24:14 +01:00
|
|
|
// ****************************************************************************
|
|
|
|
// ****************************************************************************
|
|
|
|
|
2020-04-29 22:50:06 +02:00
|
|
|
const SimpleLink = (props: SimpleLinkProps) => {
|
2020-10-20 19:10:02 +02:00
|
|
|
const { title, children, href } = props;
|
2020-04-29 22:50:06 +02:00
|
|
|
|
|
|
|
if (!href) {
|
|
|
|
return children || null;
|
|
|
|
}
|
|
|
|
|
2020-10-20 19:10:02 +02:00
|
|
|
if (!href.startsWith('lbry:/')) {
|
2020-04-29 22:50:06 +02:00
|
|
|
return (
|
2020-05-01 09:29:10 +02:00
|
|
|
<a href={href} title={title} target={'_blank'} rel={'noreferrer noopener'}>
|
2020-04-29 22:50:06 +02:00
|
|
|
{children}
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const [uri, search] = href.split('?');
|
|
|
|
const urlParams = new URLSearchParams(search);
|
|
|
|
const embed = urlParams.get('embed');
|
|
|
|
|
|
|
|
if (embed) {
|
2020-07-30 23:20:18 +02:00
|
|
|
// Decode this since users might just copy it from the url bar
|
|
|
|
const decodedUri = decodeURI(uri);
|
2020-10-20 19:10:02 +02:00
|
|
|
return (
|
2020-09-15 18:19:54 +02:00
|
|
|
<div className="embed__inline-button-preview">
|
|
|
|
<pre>{decodedUri}</pre>
|
|
|
|
</div>
|
|
|
|
);
|
2020-04-29 22:50:06 +02:00
|
|
|
}
|
|
|
|
|
2020-10-20 19:10:02 +02:00
|
|
|
// Dummy link (no 'href')
|
|
|
|
return <a title={title}>{children}</a>;
|
2020-04-29 22:50:06 +02:00
|
|
|
};
|
|
|
|
|
2021-01-27 04:24:14 +01:00
|
|
|
// ****************************************************************************
|
|
|
|
// ****************************************************************************
|
|
|
|
|
2021-04-02 09:57:37 +02:00
|
|
|
const SimpleImageLink = (props: ImageLinkProps) => {
|
|
|
|
const { src, title, alt, helpText } = props;
|
2021-10-27 20:20:47 +02:00
|
|
|
|
2021-04-02 09:57:37 +02:00
|
|
|
if (!src) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2021-11-05 20:31:51 +01:00
|
|
|
if (isEmote(title, src)) {
|
|
|
|
return <OptimizedImage src={src} title={title} className="emote" waitLoad loading="lazy" />;
|
2021-10-27 20:20:47 +02:00
|
|
|
}
|
|
|
|
|
2021-04-02 09:57:37 +02:00
|
|
|
return (
|
2021-04-08 19:14:42 +02:00
|
|
|
<Button
|
|
|
|
button="link"
|
|
|
|
iconRight={ICONS.EXTERNAL}
|
|
|
|
label={title || alt || src}
|
|
|
|
title={helpText || title || alt || src}
|
|
|
|
className="button--external-link"
|
|
|
|
href={src}
|
|
|
|
/>
|
2021-04-02 09:57:37 +02:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
// ****************************************************************************
|
|
|
|
// ****************************************************************************
|
|
|
|
|
2020-04-29 22:50:06 +02:00
|
|
|
// Use github sanitation schema
|
|
|
|
const schema = { ...defaultSchema };
|
|
|
|
|
|
|
|
// Extend sanitation schema to support lbry protocol
|
|
|
|
schema.protocols.href.push('lbry');
|
|
|
|
schema.attributes.a.push('embed');
|
|
|
|
|
2020-04-29 23:37:14 +02:00
|
|
|
const REPLACE_REGEX = /(<iframe\s+src=["'])(.*?(?=))(["']\s*><\/iframe>)/g;
|
2020-04-29 22:50:06 +02:00
|
|
|
|
2021-01-27 04:24:14 +01:00
|
|
|
// ****************************************************************************
|
|
|
|
// ****************************************************************************
|
|
|
|
|
2020-04-29 22:50:06 +02:00
|
|
|
const MarkdownPreview = (props: MarkdownProps) => {
|
2021-10-08 11:58:17 +02:00
|
|
|
const {
|
|
|
|
content,
|
|
|
|
strip,
|
|
|
|
simpleLinks,
|
|
|
|
noDataStore,
|
|
|
|
className,
|
|
|
|
parentCommentId,
|
|
|
|
isMarkdownPost,
|
|
|
|
disableTimestamps,
|
|
|
|
stakedLevel,
|
|
|
|
} = props;
|
2021-10-12 23:06:20 +02:00
|
|
|
|
2020-04-29 22:50:06 +02:00
|
|
|
const strippedContent = content
|
2021-10-12 23:06:20 +02:00
|
|
|
? content.replace(REPLACE_REGEX, (iframeHtml) => {
|
2020-04-29 23:37:14 +02:00
|
|
|
// 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://')) {
|
2020-10-20 19:10:02 +02:00
|
|
|
return src;
|
2020-04-29 23:37:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return iframeHtml;
|
2020-04-29 22:50:06 +02:00
|
|
|
})
|
|
|
|
: '';
|
|
|
|
|
|
|
|
const remarkOptions: Object = {
|
|
|
|
sanitize: schema,
|
|
|
|
fragment: React.Fragment,
|
|
|
|
remarkReactComponents: {
|
2020-10-20 19:10:02 +02:00
|
|
|
a: noDataStore
|
|
|
|
? SimpleLink
|
2021-03-09 10:33:42 +01:00
|
|
|
: (linkProps) => (
|
2020-10-20 19:10:02 +02:00
|
|
|
<MarkdownLink
|
|
|
|
{...linkProps}
|
|
|
|
parentCommentId={parentCommentId}
|
|
|
|
isMarkdownPost={isMarkdownPost}
|
2020-10-22 20:16:42 +02:00
|
|
|
simpleLinks={simpleLinks}
|
2021-04-02 09:57:37 +02:00
|
|
|
allowPreview={isStakeEnoughForPreview(stakedLevel)}
|
2020-10-20 19:10:02 +02:00
|
|
|
/>
|
|
|
|
),
|
2020-04-29 22:50:06 +02:00
|
|
|
// Workaraund of remarkOptions.Fragment
|
|
|
|
div: React.Fragment,
|
2021-11-05 20:31:51 +01:00
|
|
|
img: (imgProps) =>
|
|
|
|
isStakeEnoughForPreview(stakedLevel) && !isEmote(imgProps.title, imgProps.src) ? (
|
2021-11-08 15:08:22 +01:00
|
|
|
<ZoomableImage {...imgProps} />
|
2021-11-05 20:31:51 +01:00
|
|
|
) : (
|
|
|
|
<SimpleImageLink
|
|
|
|
src={imgProps.src}
|
|
|
|
alt={imgProps.alt}
|
|
|
|
title={imgProps.title}
|
|
|
|
helpText={
|
|
|
|
SIMPLE_SITE ? __("This channel isn't staking enough LBRY Credits for inline image previews.") : ''
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
),
|
2020-04-29 22:50:06 +02:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const remarkAttrOpts = {
|
|
|
|
scope: 'extended',
|
2020-12-18 01:37:58 +01:00
|
|
|
elements: ['link'],
|
2020-04-29 22:50:06 +02:00
|
|
|
extend: { link: ['embed'] },
|
|
|
|
defaultValue: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Strip all content and just render text
|
2021-11-08 15:08:22 +01:00
|
|
|
if (strip) {
|
2020-04-29 22:50:06 +02:00
|
|
|
// Remove new lines and extra space
|
|
|
|
remarkOptions.remarkReactComponents.p = SimpleText;
|
2021-11-08 15:08:22 +01:00
|
|
|
return (
|
2020-08-20 13:27:28 +02:00
|
|
|
<span dir="auto" className="markdown-preview">
|
2020-04-29 22:50:06 +02:00
|
|
|
{
|
|
|
|
remark()
|
|
|
|
.use(remarkStrip)
|
2020-12-17 23:44:16 +01:00
|
|
|
.use(remarkFrontMatter, ['yaml'])
|
2020-04-29 22:50:06 +02:00
|
|
|
.use(reactRenderer, remarkOptions)
|
|
|
|
.processSync(content).contents
|
|
|
|
}
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2020-09-17 17:32:31 +02:00
|
|
|
<div dir="auto" className={classnames('markdown-preview', className)}>
|
2020-04-29 22:50:06 +02:00
|
|
|
{
|
|
|
|
remark()
|
|
|
|
.use(remarkAttr, remarkAttrOpts)
|
|
|
|
// Remark plugins for lbry urls
|
|
|
|
// Note: The order is important
|
2021-10-12 23:06:20 +02:00
|
|
|
.use(formattedLinks)
|
2020-04-29 22:50:06 +02:00
|
|
|
.use(inlineLinks)
|
2021-10-08 11:58:17 +02:00
|
|
|
.use(disableTimestamps || isMarkdownPost ? null : inlineTimestamp)
|
|
|
|
.use(disableTimestamps || isMarkdownPost ? null : formattedTimestamp)
|
2020-04-29 22:50:06 +02:00
|
|
|
// Emojis
|
2021-10-27 20:20:47 +02:00
|
|
|
.use(inlineEmote)
|
|
|
|
.use(formattedEmote)
|
2020-04-29 22:50:06 +02:00
|
|
|
.use(remarkEmoji)
|
2020-05-10 06:36:33 +02:00
|
|
|
// Render new lines without needing spaces.
|
|
|
|
.use(remarkBreaks)
|
2020-12-17 23:44:16 +01:00
|
|
|
.use(remarkFrontMatter, ['yaml'])
|
2020-04-29 22:50:06 +02:00
|
|
|
.use(reactRenderer, remarkOptions)
|
|
|
|
.processSync(strippedContent).contents
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
);
|
2019-11-07 20:39:22 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
export default MarkdownPreview;
|