Extend markdown support for LBRY urls #2521
14 changed files with 465 additions and 25 deletions
|
@ -65,6 +65,7 @@
|
||||||
"@lbry/components": "^2.7.2",
|
"@lbry/components": "^2.7.2",
|
||||||
"@reach/rect": "^0.2.1",
|
"@reach/rect": "^0.2.1",
|
||||||
"@reach/tabs": "^0.1.5",
|
"@reach/tabs": "^0.1.5",
|
||||||
|
"@reach/tooltip": "^0.2.1",
|
||||||
"@types/three": "^0.93.1",
|
"@types/three": "^0.93.1",
|
||||||
"async-exit-hook": "^2.0.1",
|
"async-exit-hook": "^2.0.1",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
|
@ -163,6 +164,7 @@
|
||||||
"redux-persist-transform-filter": "0.0.16",
|
"redux-persist-transform-filter": "0.0.16",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.2.0",
|
||||||
"remark": "^9.0.0",
|
"remark": "^9.0.0",
|
||||||
|
"remark-attr": "^0.8.3",
|
||||||
"remark-emoji": "^2.0.1",
|
"remark-emoji": "^2.0.1",
|
||||||
"remark-react": "^4.0.3",
|
"remark-react": "^4.0.3",
|
||||||
"render-media": "^3.1.0",
|
"render-media": "^3.1.0",
|
||||||
|
@ -170,11 +172,13 @@
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
"stream-to-blob-url": "^2.1.1",
|
"stream-to-blob-url": "^2.1.1",
|
||||||
|
"strip-markdown": "^3.0.3",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"terser-webpack-plugin": "^1.2.3",
|
"terser-webpack-plugin": "^1.2.3",
|
||||||
"three": "^0.93.0",
|
"three": "^0.93.0",
|
||||||
"three-full": "^17.1.0",
|
"three-full": "^17.1.0",
|
||||||
"tree-kill": "^1.1.0",
|
"tree-kill": "^1.1.0",
|
||||||
|
"unist-util-visit": "^1.4.1",
|
||||||
"video.js": "^7.2.2",
|
"video.js": "^7.2.2",
|
||||||
"villain": "btzr-io/Villain",
|
"villain": "btzr-io/Villain",
|
||||||
"wavesurfer.js": "^2.2.1",
|
"wavesurfer.js": "^2.2.1",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { formatLbryUriForWeb } from 'util/uri';
|
||||||
import { OutboundLink } from 'react-ga';
|
import { OutboundLink } from 'react-ga';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClick: ?(any) => any,
|
id: ?string,
|
||||||
href: ?string,
|
href: ?string,
|
||||||
title: ?string,
|
title: ?string,
|
||||||
label: ?string,
|
label: ?string,
|
||||||
|
@ -24,6 +24,11 @@ type Props = {
|
||||||
iconSize?: number,
|
iconSize?: number,
|
||||||
constrict: ?boolean, // to shorten the button and ellipsis, only use for links
|
constrict: ?boolean, // to shorten the button and ellipsis, only use for links
|
||||||
activeClass?: string,
|
activeClass?: string,
|
||||||
|
innerRef: ?any,
|
||||||
|
// Events
|
||||||
|
onClick: ?(any) => any,
|
||||||
|
onMouseEnter: ?(any) => any,
|
||||||
|
onMouseLeave: ?(any) => any,
|
||||||
};
|
};
|
||||||
|
|
||||||
class Button extends React.PureComponent<Props> {
|
class Button extends React.PureComponent<Props> {
|
||||||
|
@ -33,7 +38,11 @@ class Button extends React.PureComponent<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
onClick,
|
onClick,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
innerRef,
|
||||||
href,
|
href,
|
||||||
title,
|
title,
|
||||||
label,
|
label,
|
||||||
|
@ -102,6 +111,7 @@ class Button extends React.PureComponent<Props> {
|
||||||
|
|
||||||
return path ? (
|
return path ? (
|
||||||
<NavLink
|
<NavLink
|
||||||
|
id={id}
|
||||||
exact
|
exact
|
||||||
to={path}
|
to={path}
|
||||||
title={title}
|
title={title}
|
||||||
|
@ -112,13 +122,17 @@ class Button extends React.PureComponent<Props> {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
className={combinedClassName}
|
className={combinedClassName}
|
||||||
activeClassName={activeClass}
|
activeClassName={activeClass}
|
||||||
|
innerRef={innerRef}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
id={id}
|
||||||
title={title}
|
title={title}
|
||||||
aria-label={description || label || title}
|
aria-label={description || label || title}
|
||||||
className={combinedClassName}
|
className={combinedClassName}
|
||||||
|
|
26
src/ui/component/claimLink/index.js
Normal file
26
src/ui/component/claimLink/index.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { doResolveUri, makeSelectTitleForUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux';
|
||||||
|
|
||||||
|
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||||
|
|
||||||
|
import ClaimLink from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => {
|
||||||
|
return {
|
||||||
|
uri: props.uri,
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
|
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||||
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(ClaimLink);
|
87
src/ui/component/claimLink/view.jsx
Normal file
87
src/ui/component/claimLink/view.jsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import PreviewLink from 'component/previewLink';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
title: ?string,
|
||||||
|
claim: StreamClaim,
|
||||||
|
children: React.Node,
|
||||||
|
className: ?string,
|
||||||
|
autoEmbed: ?boolean,
|
||||||
|
description: ?string,
|
||||||
|
isResolvingUri: boolean,
|
||||||
|
resolveUri: string => void,
|
||||||
|
blackListedOutpoints: Array<{
|
||||||
|
txid: string,
|
||||||
|
nout: number,
|
||||||
|
}>,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ClaimLink extends React.Component<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
href: null,
|
||||||
|
link: false,
|
||||||
|
title: null,
|
||||||
|
thumbnail: null,
|
||||||
|
autoEmbed: false,
|
||||||
|
description: null,
|
||||||
|
isResolvingUri: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.resolve(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.resolve(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
isClaimBlackListed() {
|
||||||
|
const { claim, blackListedOutpoints } = this.props;
|
||||||
|
|
||||||
|
if (claim && blackListedOutpoints) {
|
||||||
|
let blackListed = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < blackListedOutpoints.length; i += 1) {
|
||||||
|
const outpoint = blackListedOutpoints[i];
|
||||||
|
if (outpoint.txid === claim.txid && outpoint.nout === claim.nout) {
|
||||||
|
blackListed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blackListed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve = (props: Props) => {
|
||||||
|
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||||
|
|
||||||
|
if (!isResolvingUri && claim === undefined && uri) {
|
||||||
|
resolveUri(uri);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { uri, claim, title, className, autoEmbed, children, isResolvingUri } = this.props;
|
||||||
|
const isUnresolved = (!isResolvingUri && !claim) || !claim;
|
||||||
|
const isBlacklisted = this.isClaimBlackListed();
|
||||||
|
|
||||||
|
if (isBlacklisted || isUnresolved) {
|
||||||
|
return <span>{children}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name: claimName } = claim;
|
||||||
|
const showPreview = autoEmbed === true && !isUnresolved;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button label={children} title={title || claimName} button={'link'} navigate={uri} className={className} />
|
||||||
|
{showPreview && <PreviewLink uri={uri} />}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClaimLink;
|
||||||
Should be called Should be called `ClaimLink` ?
Done 👍 Done :+1:
|
|
@ -2,11 +2,12 @@
|
||||||
import type { ElementRef } from 'react';
|
import type { ElementRef } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOMServer from 'react-dom/server';
|
import ReactDOMServer from 'react-dom/server';
|
||||||
import 'easymde/dist/easymde.min.css';
|
|
||||||
import Toggle from 'react-toggle';
|
import Toggle from 'react-toggle';
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
import MarkdownPreview from 'component/common/markdown-preview-internal';
|
import MarkdownPreview from 'component/common/markdown-preview-internal';
|
||||||
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
|
|
||||||
|
import 'easymde/dist/easymde.min.css';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import remark from 'remark';
|
import remark from 'remark';
|
||||||
import reactRenderer from 'remark-react';
|
import remarkAttr from 'remark-attr';
|
||||||
|
import remarkStrip from 'strip-markdown';
|
||||||
import remarkEmoji from 'remark-emoji';
|
import remarkEmoji from 'remark-emoji';
|
||||||
|
import reactRenderer from 'remark-react';
|
||||||
import ExternalLink from 'component/externalLink';
|
import ExternalLink from 'component/externalLink';
|
||||||
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
||||||
|
import { formatedLinks, inlineLinks } from 'util/remark-lbry';
|
||||||
|
|
||||||
type MarkdownProps = {
|
type SimpleTextProps = {
|
||||||
content: ?string,
|
children?: React.Node,
|
||||||
promptLinks?: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type SimpleLinkProps = {
|
type SimpleLinkProps = {
|
||||||
|
@ -17,6 +19,16 @@ type SimpleLinkProps = {
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MarkdownProps = {
|
||||||
|
strip?: boolean,
|
||||||
|
content: ?string,
|
||||||
|
promptLinks?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SimpleText = (props: SimpleTextProps) => {
|
||||||
|
return <span>{props.children}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
const SimpleLink = (props: SimpleLinkProps) => {
|
const SimpleLink = (props: SimpleLinkProps) => {
|
||||||
const { href, title, children } = props;
|
const { href, title, children } = props;
|
||||||
return (
|
return (
|
||||||
|
@ -31,19 +43,54 @@ const schema = { ...defaultSchema };
|
||||||
|
|
||||||
// Extend sanitation schema to support lbry protocol
|
// Extend sanitation schema to support lbry protocol
|
||||||
schema.protocols.href.push('lbry');
|
schema.protocols.href.push('lbry');
|
||||||
|
schema.attributes.a.push('embed');
|
||||||
|
|
||||||
const MarkdownPreview = (props: MarkdownProps) => {
|
const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
const { content, promptLinks } = props;
|
const { content, strip, promptLinks } = props;
|
||||||
const remarkOptions = {
|
|
||||||
|
const remarkOptions: Object = {
|
||||||
sanitize: schema,
|
sanitize: schema,
|
||||||
|
fragment: React.Fragment,
|
||||||
remarkReactComponents: {
|
remarkReactComponents: {
|
||||||
a: promptLinks ? ExternalLink : SimpleLink,
|
a: promptLinks ? ExternalLink : SimpleLink,
|
||||||
|
// Workaraund of remarkOptions.Fragment
|
||||||
|
div: React.Fragment,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 className="markdown-preview">
|
||||||
|
{
|
||||||
|
remark()
|
||||||
|
.use(remarkStrip)
|
||||||
|
.use(reactRenderer, remarkOptions)
|
||||||
|
.processSync(content).contents
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="markdown-preview">
|
<div className="markdown-preview">
|
||||||
{
|
{
|
||||||
remark()
|
remark()
|
||||||
|
.use(remarkAttr, remarkAttrOpts)
|
||||||
|
// Remark plugins for lbry urls
|
||||||
|
// Note: The order is important
|
||||||
|
.use(formatedLinks)
|
||||||
|
.use(inlineLinks)
|
||||||
|
// Emojis
|
||||||
.use(remarkEmoji)
|
.use(remarkEmoji)
|
||||||
.use(reactRenderer, remarkOptions)
|
.use(reactRenderer, remarkOptions)
|
||||||
.processSync(content).contents
|
.processSync(content).contents
|
||||||
|
|
|
@ -2,14 +2,24 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
text: ?string,
|
text?: ?string,
|
||||||
lines: number,
|
lines: number,
|
||||||
|
showTooltip?: boolean,
|
||||||
|
children?: React.Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TruncatedText = (props: Props) => (
|
const TruncatedText = (props: Props) => {
|
||||||
<span title={props.text} className="truncated-text" style={{ WebkitLineClamp: props.lines }}>
|
const { text, children, lines, showTooltip } = props;
|
||||||
{props.text}
|
const tooltip = showTooltip ? children || text : '';
|
||||||
|
return (
|
||||||
|
<span title={tooltip} className="truncated-text" style={{ WebkitLineClamp: lines }}>
|
||||||
|
{children || text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TruncatedText.defaultProps = {
|
||||||
|
showTooltip: true,
|
||||||
|
};
|
||||||
|
|
||||||
export default TruncatedText;
|
export default TruncatedText;
|
||||||
|
|
|
@ -4,10 +4,12 @@ import * as ICONS from 'constants/icons';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { isURIValid } from 'lbry-redux';
|
import { isURIValid } from 'lbry-redux';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import ClaimLink from 'component/claimLink';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
href: string,
|
href: string,
|
||||||
title?: string,
|
title?: string,
|
||||||
|
embed?: boolean,
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
openModal: (id: string, { uri: string }) => void,
|
openModal: (id: string, { uri: string }) => void,
|
||||||
};
|
};
|
||||||
|
@ -16,18 +18,16 @@ class ExternalLink extends React.PureComponent<Props> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
href: null,
|
href: null,
|
||||||
title: null,
|
title: null,
|
||||||
|
embed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
createLink() {
|
createLink() {
|
||||||
const { href, title, children, openModal } = this.props;
|
const { href, title, embed, children, openModal } = this.props;
|
||||||
|
|
||||||
// Regex for url protocol
|
// Regex for url protocol
|
||||||
const protocolRegex = new RegExp('^(https?|lbry|mailto)+:', 'i');
|
const protocolRegex = new RegExp('^(https?|lbry|mailto)+:', 'i');
|
||||||
const protocol = href ? protocolRegex.exec(href) : null;
|
const protocol = href ? protocolRegex.exec(href) : null;
|
||||||
|
|
||||||
// Return plain text if no valid url
|
// Return plain text if no valid url
|
||||||
let element = <span>{children}</span>;
|
let element = <span>{children}</span>;
|
||||||
|
|
||||||
// Return external link if protocol is http or https
|
// Return external link if protocol is http or https
|
||||||
if (protocol && (protocol[0] === 'http:' || protocol[0] === 'https:' || protocol[0] === 'mailto:')) {
|
if (protocol && (protocol[0] === 'http:' || protocol[0] === 'https:' || protocol[0] === 'mailto:')) {
|
||||||
element = (
|
element = (
|
||||||
|
@ -41,10 +41,13 @@ class ExternalLink extends React.PureComponent<Props> {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return local link if protocol is lbry uri
|
// Return local link if protocol is lbry uri
|
||||||
if (protocol && protocol[0] === 'lbry:' && isURIValid(href)) {
|
if (protocol && protocol[0] === 'lbry:' && isURIValid(href)) {
|
||||||
element = <Button button="link" title={title || href} label={children} navigate={href} />;
|
element = (
|
||||||
|
<ClaimLink uri={href} autoEmbed={embed}>
|
||||||
|
{children}
|
||||||
|
</ClaimLink>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
|
|
37
src/ui/component/previewLink/index.js
Normal file
37
src/ui/component/previewLink/index.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
doResolveUri,
|
||||||
|
makeSelectClaimIsMine,
|
||||||
|
makeSelectTitleForUri,
|
||||||
|
makeSelectThumbnailForUri,
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectIsUriResolving,
|
||||||
|
makeSelectMetadataItemForUri,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
|
||||||
|
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||||
|
|
||||||
|
import PreviewLink from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => {
|
||||||
|
return {
|
||||||
|
uri: props.uri,
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
|
description: makeSelectMetadataItemForUri(props.uri, 'description')(state),
|
||||||
|
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||||
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(PreviewLink);
|
58
src/ui/component/previewLink/view.jsx
Normal file
58
src/ui/component/previewLink/view.jsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
import TruncatedText from 'component/common/truncated-text';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import { formatLbryUriForWeb } from 'util/uri';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
title: ?string,
|
||||||
|
thumbnail: ?string,
|
||||||
|
description: ?string,
|
||||||
|
history: { push: string => void },
|
||||||
|
};
|
||||||
|
|
||||||
|
class PreviewLink extends React.PureComponent<Props> {
|
||||||
|
handleClick = () => {
|
||||||
|
const { uri, history } = this.props;
|
||||||
|
history.push(formatLbryUriForWeb(uri));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { uri, title, description, thumbnail } = this.props;
|
||||||
|
const placeholder = 'static/img/placeholder.png';
|
||||||
|
|
||||||
|
const thumbnailStyle = {
|
||||||
|
backgroundImage: `url(${thumbnail || placeholder})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={'preview-link'} role="button" onClick={this.handleClick}>
|
||||||
|
<span className={'file-list__item'}>
|
||||||
|
<span style={thumbnailStyle} className={'preview-link__thumbnail media__thumb'} />
|
||||||
|
<span className={'file-list__item-metadata'}>
|
||||||
|
<span className={'file-list__item-info'}>
|
||||||
|
<span className={'file-list__item-title'}>
|
||||||
|
<TruncatedText text={title} lines={1} />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className={'preview-link__description media__subtext'}>
|
||||||
|
<UriIndicator uri={uri} link />
|
||||||
|
</span>
|
||||||
|
<span className={'file-list__item-properties'}>
|
||||||
|
<span className={'preview-link__description media__subtext'}>
|
||||||
|
<TruncatedText lines={2} showTooltip={false}>
|
||||||
|
<MarkdownPreview content={description} promptLinks strip />
|
||||||
|
</TruncatedText>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(PreviewLink);
|
|
@ -34,8 +34,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-preview.editor-preview-active {
|
.editor-preview.editor-preview-active {
|
||||||
background-color: $lbry-gray-5;
|
background-color: $lbry-black;
|
||||||
color: $lbry-black;
|
color: $lbry-white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,4 +120,23 @@
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-link {
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
background-color: rgba($lbry-teal-5, 0.1);
|
||||||
|
border-left: 0.5rem solid $lbry-teal-5;
|
||||||
|
display: block;
|
||||||
|
align-items: center;
|
||||||
|
width: 40rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-link__thumbnail {
|
||||||
|
width: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-link__description {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
111
src/ui/util/remark-lbry.js
Normal file
111
src/ui/util/remark-lbry.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import { parseURI } from 'lbry-redux';
|
||||||
|
import visit from 'unist-util-visit';
|
||||||
|
|
||||||
|
const protocol = 'lbry://';
|
||||||
|
const locateURI = (value, fromIndex) => value.indexOf(protocol, fromIndex);
|
||||||
|
const locateMention = (value, fromIndex) => value.indexOf('@', fromIndex);
|
||||||
|
|
||||||
|
// Generate a valid markdown link
|
||||||
|
const createURI = (text, uri, embed = false) => ({
|
||||||
|
type: 'link',
|
||||||
|
url: (uri.startsWith(protocol) ? '' : protocol) + uri,
|
||||||
|
data: {
|
||||||
|
// Custom attribute
|
||||||
|
hProperties: { embed },
|
||||||
|
},
|
||||||
|
children: [{ type: 'text', value: text }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateURI = (match, eat) => {
|
||||||
|
if (match) {
|
||||||
|
try {
|
||||||
|
const text = match[0];
|
||||||
|
const uri = parseURI(text);
|
||||||
|
const isValid = uri && uri.claimName;
|
||||||
|
const isChannel = uri.isChannel && !uri.path;
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
// Create channel link
|
||||||
|
if (isChannel) {
|
||||||
|
return eat(text)(createURI(uri.claimName, text, false));
|
||||||
|
}
|
||||||
|
// Create claim link
|
||||||
|
return eat(text)(createURI(text, text, true));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Silent errors: console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate a markdown link from channel name
|
||||||
|
function tokenizeMention(eat, value, silent) {
|
||||||
|
const match = /^@+[a-zA-Z0-9-#:/]+/.exec(value);
|
||||||
This should use This should use `regexValidUri` and `isValidUri()` from `lbry-redux`
I can't find I can't find `regexValidUri`, did you mean `regexInvalidURI` ?
https://github.com/lbryio/lbry-redux/blob/master/src/lbryURI.js#L5
this? https://github.com/lbryio/lbry-redux/blob/master/src/lbryURI.js#L32
whoops. yes whoops. yes
@seanyesmunt not sure how this is done on reach-ui tooltip 🙃 I mean the delay time to show / hide. @seanyesmunt not sure how this is done on reach-ui tooltip :upside_down_face:
I mean the delay time to show / hide.
this is are already implemented inside this is are already implemented inside `parseURI`:
https://github.com/lbryio/lbry-desktop/blob/b7adc597e232f95dea200a6f49bf5d17c1af102c/src/ui/util/remark-lbry.js#L23
|
|||||||
|
return validateURI(match, eat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a markdown link from lbry url
|
||||||
|
function tokenizeURI(eat, value, silent) {
|
||||||
|
const match = /^(lbry:\/\/)+[a-zA-Z0-9-@#:/]+/.exec(value);
|
||||||
|
return validateURI(match, eat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure tokenizer for lbry urls
|
||||||
|
tokenizeURI.locator = locateURI;
|
||||||
|
tokenizeURI.notInList = true;
|
||||||
|
tokenizeURI.notInLink = true;
|
||||||
|
tokenizeURI.notInBlock = true;
|
||||||
|
|
||||||
|
// Configure tokenizer for lbry channels
|
||||||
|
tokenizeMention.locator = locateMention;
|
||||||
|
tokenizeMention.notInList = true;
|
||||||
|
tokenizeMention.notInLink = true;
|
||||||
|
tokenizeMention.notInBlock = true;
|
||||||
|
|
||||||
|
const visitor = (node, index, parent) => {
|
||||||
|
if (node.type === 'link' && parent && parent.type === 'paragraph') {
|
||||||
|
try {
|
||||||
|
const uri = parseURI(node.url);
|
||||||
|
const isValid = uri && uri.claimName;
|
||||||
|
const isChannel = uri.isChannel && !uri.path;
|
||||||
|
if (isValid && !isChannel) {
|
||||||
|
if (!node.data || !node.data.hProperties) {
|
||||||
|
// Create new node data
|
||||||
|
node.data = {
|
||||||
|
hProperties: { embed: true },
|
||||||
|
};
|
||||||
|
} else if (node.data.hProperties) {
|
||||||
|
// Don't overwrite current attributes
|
||||||
|
node.data.hProperties = {
|
||||||
|
embed: true,
|
||||||
|
...node.data.hProperties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Silent errors: console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// transform
|
||||||
|
const transform = tree => {
|
||||||
|
visit(tree, ['link'], visitor);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatedLinks = () => transform;
|
||||||
|
|
||||||
|
// Main module
|
||||||
|
export function inlineLinks() {
|
||||||
|
const Parser = this.Parser;
|
||||||
|
const tokenizers = Parser.prototype.inlineTokenizers;
|
||||||
|
const methods = Parser.prototype.inlineMethods;
|
||||||
|
|
||||||
|
// Add an inline tokenizer (defined in the following example).
|
||||||
|
tokenizers.uri = tokenizeURI;
|
||||||
|
tokenizers.mention = tokenizeMention;
|
||||||
|
|
||||||
|
// Run it just before `text`.
|
||||||
|
methods.splice(methods.indexOf('text'), 0, 'uri');
|
||||||
|
methods.splice(methods.indexOf('text'), 0, 'mention');
|
||||||
|
}
|
25
yarn.lock
25
yarn.lock
|
@ -5498,6 +5498,11 @@ html-comment-regex@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
|
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
|
||||||
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
|
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
|
||||||
|
|
||||||
|
html-element-attributes@^2.0.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/html-element-attributes/-/html-element-attributes-2.1.0.tgz#ff674b8716526b8a9ee1d8e454466eda56d6d0cc"
|
||||||
|
integrity sha512-uWNlZuzM3MfRvZLAHWUzwwU6He2NvX9szpXwjG7+hoVqZzY3e0vc6mlujiSnJ/i9zVFZDhBDN33Pm9HNEzcyPg==
|
||||||
|
|
||||||
html-entities@^1.2.0, html-entities@^1.2.1:
|
html-entities@^1.2.0, html-entities@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
|
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
|
||||||
|
@ -7114,6 +7119,11 @@ math-expression-evaluator@^1.2.14:
|
||||||
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
|
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
|
||||||
integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw=
|
integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw=
|
||||||
|
|
||||||
|
md-attr-parser@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/md-attr-parser/-/md-attr-parser-1.2.1.tgz#1043f6451c33ed3e392b40504df08010f4d03e3d"
|
||||||
|
integrity sha512-dZqt2L4Q7FUcx6ZcuownAxa74Y7d5jcsHRB2MIgQ0vT10Pa+/0Som6hhJ+jgAjP3vnFtrd4aO+ZMc5K7QVfbiQ==
|
||||||
|
|
||||||
md5.js@^1.3.4:
|
md5.js@^1.3.4:
|
||||||
version "1.3.5"
|
version "1.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
|
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
|
||||||
|
@ -9841,6 +9851,14 @@ relateurl@0.2.x:
|
||||||
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||||
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
||||||
|
|
||||||
|
remark-attr@^0.8.3:
|
||||||
|
version "0.8.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/remark-attr/-/remark-attr-0.8.3.tgz#0263045acb958e1556b0c6955b5d66a4b36d4d92"
|
||||||
|
integrity sha512-JMP6rmLwhj5VmDRG99kGr+HNXQ6MmAs+UojeAHGMQFiIitEmJZBkmgOBsWadxu/RTACD4XnMuJXY1QWEFdw31A==
|
||||||
|
dependencies:
|
||||||
|
html-element-attributes "^2.0.0"
|
||||||
|
md-attr-parser "^1.2.1"
|
||||||
|
|
||||||
remark-emoji@^2.0.1:
|
remark-emoji@^2.0.1:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-2.0.2.tgz#49c134021132c192ee4cceed1988ec9b8ced7eb8"
|
resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-2.0.2.tgz#49c134021132c192ee4cceed1988ec9b8ced7eb8"
|
||||||
|
@ -10912,6 +10930,11 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||||
|
|
||||||
|
strip-markdown@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-markdown/-/strip-markdown-3.0.3.tgz#93b4526abe32a1d69e5ca943f4d9ba25c01a4d48"
|
||||||
|
integrity sha512-G2DSM9wy3PWxY3miAibWpsTqZgXLXgRoq0yVyaVs9O7FDGEwPO6pmSE8CyzHhU88Z2w1dkFqFmWUklFNsQKwqg==
|
||||||
|
|
||||||
style-loader@^0.23.1:
|
style-loader@^0.23.1:
|
||||||
version "0.23.1"
|
version "0.23.1"
|
||||||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925"
|
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925"
|
||||||
|
@ -11564,7 +11587,7 @@ unist-util-visit-parents@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
unist-util-is "^2.1.2"
|
unist-util-is "^2.1.2"
|
||||||
|
|
||||||
unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.3.0, unist-util-visit@^1.4.0:
|
unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.3.0, unist-util-visit@^1.4.0, unist-util-visit@^1.4.1:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"
|
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"
|
||||||
integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==
|
integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==
|
||||||
|
|
Loading…
Reference in a new issue
This component is very similar to
ChannelLink
, they should probably just be combined and separated by a prop