Extend markdown support for LBRY urls #2521
7 changed files with 284 additions and 2 deletions
|
@ -147,6 +147,7 @@
|
||||||
"react-hot-loader": "^4.7.2",
|
"react-hot-loader": "^4.7.2",
|
||||||
"react-modal": "^3.1.7",
|
"react-modal": "^3.1.7",
|
||||||
"react-paginate": "^5.2.1",
|
"react-paginate": "^5.2.1",
|
||||||
|
"react-portal-tooltip": "^2.4.7",
|
||||||
"react-pose": "^4.0.5",
|
"react-pose": "^4.0.5",
|
||||||
"react-redux": "^6.0.1",
|
"react-redux": "^6.0.1",
|
||||||
"react-router": "^5.0.0",
|
"react-router": "^5.0.0",
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { formatLbryUriForWeb } from 'util/uri';
|
||||||
import { OutboundLink } from 'react-ga';
|
import { OutboundLink } from 'react-ga';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClick: ?(any) => any,
|
|
||||||
href: ?string,
|
href: ?string,
|
||||||
title: ?string,
|
title: ?string,
|
||||||
label: ?string,
|
label: ?string,
|
||||||
|
@ -24,6 +23,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> {
|
||||||
|
@ -34,6 +38,9 @@ class Button extends React.PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
onClick,
|
onClick,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
innerRef,
|
||||||
href,
|
href,
|
||||||
title,
|
title,
|
||||||
label,
|
label,
|
||||||
|
@ -111,8 +118,11 @@ 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>
|
||||||
|
|
39
src/ui/component/channelLink/index.js
Normal file
39
src/ui/component/channelLink/index.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
doResolveUri,
|
||||||
|
makeSelectClaimIsMine,
|
||||||
|
makeSelectTitleForUri,
|
||||||
|
makeSelectThumbnailForUri,
|
||||||
|
makeSelectCoverForUri,
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectIsUriResolving,
|
||||||
|
makeSelectMetadataItemForUri,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
|
||||||
|
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||||
|
|
||||||
|
import ChannelLink from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => {
|
||||||
|
return {
|
||||||
|
uri: props.uri,
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
|
cover: makeSelectCoverForUri(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
|
||||||
|
)(ChannelLink);
|
163
src/ui/component/channelLink/view.jsx
Normal file
163
src/ui/component/channelLink/view.jsx
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import { parseURI } from 'lbry-redux';
|
||||||
|
import ToolTip from 'react-portal-tooltip';
|
||||||
|
import Button from 'component/button';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
title: ?string,
|
||||||
|
cover: ?string,
|
||||||
|
claim: StreamClaim,
|
||||||
|
children: React.Node,
|
||||||
|
thumbnail: ?string,
|
||||||
|
description: ?string,
|
||||||
|
isResolvingUri: boolean,
|
||||||
|
resolveUri: string => void,
|
||||||
|
blackListedOutpoints: Array<{
|
||||||
|
txid: string,
|
||||||
|
nout: number,
|
||||||
|
}>,
|
||||||
|
};
|
||||||
|
|
||||||
|
type TooltipProps = {
|
||||||
|
uri: string,
|
||||||
|
style: Object,
|
||||||
|
title: ?string,
|
||||||
|
active: ?boolean,
|
||||||
|
parent: ?HTMLElement,
|
||||||
|
claimId: ?string,
|
||||||
|
thumbnail: ?string,
|
||||||
|
claimName: ?string,
|
||||||
|
channelName: ?string,
|
||||||
|
description: ?string,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
isTooltipActive: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChannelTooltip = (props: TooltipProps) => {
|
||||||
|
const { style, title, active, parent, claimId, thumbnail, claimName, channelName, description } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolTip active={active} position="bottom" style={style} arrow="left" align="left" parent={parent}>
|
||||||
|
<div className={'channel-tooltip'}>
|
||||||
|
<div className={'channel-tooltip__info'}>
|
||||||
|
<img className={'channel-tooltip__thumbnail'} src={thumbnail} />
|
||||||
|
<div>
|
||||||
|
<h2 className={'channel-tooltip__title'}>{title || channelName}</h2>
|
||||||
|
<h3 className={'channel-tooltip__url'}>
|
||||||
|
{claimName}
|
||||||
|
{claimId && `#${claimId}`}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'channel-tooltip__description'}>
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
<div className={'channel-tooltip__stats'} />
|
||||||
|
</div>
|
||||||
|
</ToolTip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChannelLink extends React.Component<Props, State> {
|
||||||
|
buttonRef: { current: ?any };
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
href: null,
|
||||||
|
title: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { isTooltipActive: false };
|
||||||
|
(this: any).buttonRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
showTooltip = () => {
|
||||||
|
this.setState({ isTooltipActive: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
hideTooltip = () => {
|
||||||
|
this.setState({ isTooltipActive: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { isResolvingUri, resolveUri, uri } = this.props;
|
||||||
|
if (!isResolvingUri) {
|
||||||
|
resolveUri(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
const { isResolvingUri, resolveUri, claim, uri } = this.props;
|
||||||
|
if (!isResolvingUri && uri && claim === undefined) {
|
||||||
|
resolveUri(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { uri, claim, title, description, thumbnail, children, isResolvingUri } = this.props;
|
||||||
|
const { channelName, claimName, claimId } = parseURI(uri);
|
||||||
|
const blackListed = this.isClaimBlackListed();
|
||||||
|
const isReady = !blackListed && !isResolvingUri && claim !== null;
|
||||||
|
const tooltipReady = isReady && this.buttonRef.current !== null;
|
||||||
|
|
||||||
|
const bgColor = '#32373b';
|
||||||
|
|
||||||
|
const tooltipStyle = {
|
||||||
|
style: { background: bgColor },
|
||||||
|
arrowStyle: { color: bgColor },
|
||||||
|
};
|
||||||
|
|
||||||
|
return isReady ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
label={children}
|
||||||
|
navigate={uri}
|
||||||
|
innerRef={this.buttonRef}
|
||||||
|
onMouseEnter={this.showTooltip}
|
||||||
|
onMouseLeave={this.hideTooltip}
|
||||||
|
/>
|
||||||
|
{tooltipReady && (
|
||||||
|
<ChannelTooltip
|
||||||
|
uri={uri}
|
||||||
|
style={tooltipStyle}
|
||||||
|
title={title}
|
||||||
|
claimId={claimId}
|
||||||
|
claimName={claimName}
|
||||||
|
channelName={channelName}
|
||||||
|
thumbnail={thumbnail}
|
||||||
|
description={description}
|
||||||
|
active={this.state.isTooltipActive}
|
||||||
|
parent={this.buttonRef.current}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<span>{children}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChannelLink;
|
|
@ -4,6 +4,7 @@ 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 ChannelLink from 'component/channelLink';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
href: string,
|
href: string,
|
||||||
|
@ -40,7 +41,7 @@ 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 = <ChannelLink uri={href}>{children}</ChannelLink>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
|
|
|
@ -85,3 +85,66 @@ $metadata-z-index: 1;
|
||||||
margin-top: -0.25rem;
|
margin-top: -0.25rem;
|
||||||
color: rgba($lbry-white, 0.75);
|
color: rgba($lbry-white, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tooltip
|
||||||
|
|
||||||
|
.channel-tooltip__thumbnail {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
background: #fff;
|
||||||
|
margin-right: 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
background-size: cover;
|
||||||
|
box-shadow: 0px 8px 40px -3px $lbry-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-tooltip {
|
||||||
|
width: 18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-tooltip__description {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-tooltip__description p {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
/*
|
||||||
|
This fixes the line-clamp issue on resize:
|
||||||
|
https://stackoverflow.com/questions/38989475/css-multi-line-line-clamp-ellipsis-doesnt-work
|
||||||
|
*/
|
||||||
|
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-tooltip__info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-tooltip__title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.5em;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
color: $lbry-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-tooltip__url {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -9432,6 +9432,11 @@ react-paginate@^5.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.6.1"
|
prop-types "^15.6.1"
|
||||||
|
|
||||||
|
react-portal-tooltip@^2.4.7:
|
||||||
|
version "2.4.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-portal-tooltip/-/react-portal-tooltip-2.4.7.tgz#77fd4a75760afecd116736c480485aa22cbbb740"
|
||||||
|
integrity sha512-ZOuCKpvu8rr6aS6nNjckd78qEgWyHZnKidZ9Yqrd0HR5K9NYJ7csb4duc0kEXeG8DiCWsLPU70o1EVP+MxlINg==
|
||||||
|
|
||||||
react-pose@^4.0.5:
|
react-pose@^4.0.5:
|
||||||
version "4.0.8"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/react-pose/-/react-pose-4.0.8.tgz#91bdfafde60e4096e7878a35dcc77715bed68f24"
|
resolved "https://registry.yarnpkg.com/react-pose/-/react-pose-4.0.8.tgz#91bdfafde60e4096e7878a35dcc77715bed68f24"
|
||||||
|
|
Loading…
Reference in a new issue