refactor floatingUri to allow inline players in comments/markdown
This commit is contained in:
parent
3b20104261
commit
295b8cf2e1
49 changed files with 540 additions and 331 deletions
|
@ -40,6 +40,7 @@ DEFAULT_LANGUAGE=en
|
||||||
# Additional settings for below are found in ui/constants/settings and are for
|
# Additional settings for below are found in ui/constants/settings and are for
|
||||||
# preventing user settings from applying to custom sites without overwriting them.
|
# preventing user settings from applying to custom sites without overwriting them.
|
||||||
# UNSYNCED_SETTINGS='theme dark_mode_times automatic_dark_mode_enabled'
|
# UNSYNCED_SETTINGS='theme dark_mode_times automatic_dark_mode_enabled'
|
||||||
|
KNOWN_APP_DOMAINS=lbry.tv,lbry.lat,odysee.com
|
||||||
|
|
||||||
# Custom Content
|
# Custom Content
|
||||||
# If the following is true, copy custom/homepage.example.js to custom/homepage.js and modify
|
# If the following is true, copy custom/homepage.example.js to custom/homepage.js and modify
|
||||||
|
|
|
@ -37,6 +37,7 @@ const config = {
|
||||||
PINNED_LABEL_1: process.env.PINNED_LABEL_1,
|
PINNED_LABEL_1: process.env.PINNED_LABEL_1,
|
||||||
PINNED_URI_2: process.env.PINNED_URI_2,
|
PINNED_URI_2: process.env.PINNED_URI_2,
|
||||||
PINNED_LABEL_2: process.env.PINNED_LABEL_2,
|
PINNED_LABEL_2: process.env.PINNED_LABEL_2,
|
||||||
|
KNOWN_APP_DOMAINS: process.env.KNOWN_APP_DOMAINS
|
||||||
};
|
};
|
||||||
|
|
||||||
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;
|
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;
|
||||||
|
|
8
flow-typed/content.js
vendored
Normal file
8
flow-typed/content.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
declare type PlayingUri = {
|
||||||
|
uri: string,
|
||||||
|
pathname: string,
|
||||||
|
commentId?: string,
|
||||||
|
source?: string,
|
||||||
|
};
|
|
@ -136,7 +136,7 @@
|
||||||
"imagesloaded": "^4.1.4",
|
"imagesloaded": "^4.1.4",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#15737f9b098f3654122a75f93d63584e7ffa17a1",
|
"lbry-redux": "lbryio/lbry-redux#823197af37da745fd79632b9e82deddf9d7fe033",
|
||||||
"lbryinc": "lbryio/lbryinc#db0663fcc4a64cb082b6edc5798fafa67eb4300f",
|
"lbryinc": "lbryio/lbryinc#db0663fcc4a64cb082b6edc5798fafa67eb4300f",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
|
|
|
@ -1329,7 +1329,7 @@
|
||||||
"Replying as": "Replying as",
|
"Replying as": "Replying as",
|
||||||
"No uploads": "No uploads",
|
"No uploads": "No uploads",
|
||||||
"uploads": "uploads",
|
"uploads": "uploads",
|
||||||
"Discussion": "Discussion",
|
"Community": "Community",
|
||||||
"Staked LBRY Credits": "Staked LBRY Credits",
|
"Staked LBRY Credits": "Staked LBRY Credits",
|
||||||
"1 comment": "1 comment",
|
"1 comment": "1 comment",
|
||||||
"%total_comments% comments": "%total_comments% comments",
|
"%total_comments% comments": "%total_comments% comments",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doResolveUri, makeSelectTitleForUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux';
|
import { doResolveUri, makeSelectTitleForUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux';
|
||||||
import { selectBlackListedOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||||
|
import { selectPlayingUri } from 'redux/selectors/content';
|
||||||
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import ClaimLink from './view';
|
import ClaimLink from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -10,11 +12,11 @@ const select = (state, props) => {
|
||||||
title: makeSelectTitleForUri(props.uri)(state),
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
|
playingUri: selectPlayingUri(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = dispatch => ({
|
export default connect(select, {
|
||||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
doResolveUri,
|
||||||
});
|
doSetPlayingUri,
|
||||||
|
})(ClaimLink);
|
||||||
export default connect(select, perform)(ClaimLink);
|
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import PreviewLink from 'component/previewLink';
|
import classnames from 'classnames';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import ClaimPreview from 'component/claimPreview';
|
||||||
|
import EmbedPlayButton from 'component/embedPlayButton';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import { INLINE_PLAYER_WRAPPER_CLASS } from 'component/fileRenderFloating/view';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
autoEmbed: ?boolean,
|
|
||||||
description: ?string,
|
description: ?string,
|
||||||
isResolvingUri: boolean,
|
isResolvingUri: boolean,
|
||||||
resolveUri: string => void,
|
doResolveUri: string => void,
|
||||||
blackListedOutpoints: Array<{
|
blackListedOutpoints: Array<{
|
||||||
txid: string,
|
txid: string,
|
||||||
nout: number,
|
nout: number,
|
||||||
}>,
|
}>,
|
||||||
|
playingUri: ?PlayingUri,
|
||||||
|
parentCommentId?: string,
|
||||||
|
isMarkdownPost?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
class ClaimLink extends React.Component<Props> {
|
class ClaimLink extends React.Component<Props> {
|
||||||
|
@ -22,7 +27,6 @@ class ClaimLink extends React.Component<Props> {
|
||||||
href: null,
|
href: null,
|
||||||
link: false,
|
link: false,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
autoEmbed: false,
|
|
||||||
description: null,
|
description: null,
|
||||||
isResolvingUri: false,
|
isResolvingUri: false,
|
||||||
};
|
};
|
||||||
|
@ -51,17 +55,22 @@ class ClaimLink extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve = (props: Props) => {
|
resolve = (props: Props) => {
|
||||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
const { isResolvingUri, doResolveUri, claim, uri } = props;
|
||||||
|
|
||||||
if (!isResolvingUri && claim === undefined && uri) {
|
if (!isResolvingUri && claim === undefined && uri) {
|
||||||
resolveUri(uri);
|
doResolveUri(uri);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { uri, claim, autoEmbed, children, isResolvingUri } = this.props;
|
const { uri, claim, children, isResolvingUri, playingUri, parentCommentId, isMarkdownPost } = this.props;
|
||||||
const isUnresolved = (!isResolvingUri && !claim) || !claim;
|
const isUnresolved = (!isResolvingUri && !claim) || !claim;
|
||||||
const isBlacklisted = this.isClaimBlackListed();
|
const isBlacklisted = this.isClaimBlackListed();
|
||||||
|
const isPlayingInline =
|
||||||
|
playingUri &&
|
||||||
|
playingUri.uri === uri &&
|
||||||
|
((playingUri.source === 'comment' && parentCommentId === playingUri.commentId) ||
|
||||||
|
playingUri.source === 'markdown');
|
||||||
|
|
||||||
if (isBlacklisted || isUnresolved) {
|
if (isBlacklisted || isUnresolved) {
|
||||||
return <span>{children}</span>;
|
return <span>{children}</span>;
|
||||||
|
@ -69,13 +78,23 @@ class ClaimLink extends React.Component<Props> {
|
||||||
|
|
||||||
const { value_type: valueType } = claim;
|
const { value_type: valueType } = claim;
|
||||||
const isChannel = valueType === 'channel';
|
const isChannel = valueType === 'channel';
|
||||||
const showPreview = autoEmbed === true && !isUnresolved;
|
|
||||||
|
|
||||||
if (isChannel) {
|
return isChannel ? (
|
||||||
return <UriIndicator uri={uri} link addTooltip />;
|
<div className="card--inline">
|
||||||
}
|
<ClaimPreview uri={uri} wrapperElement="div" />
|
||||||
|
</div>
|
||||||
return <React.Fragment>{showPreview && <PreviewLink uri={uri} />}</React.Fragment>;
|
) : (
|
||||||
|
<div className={classnames('claim-link')}>
|
||||||
|
<div
|
||||||
|
className={classnames({
|
||||||
|
[INLINE_PLAYER_WRAPPER_CLASS]: isPlayingInline,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<EmbedPlayButton uri={uri} parentCommentId={parentCommentId} isMarkdownPost={isMarkdownPost} />
|
||||||
|
</div>
|
||||||
|
<Button button="link" className="preview-link__url" label={uri} navigate={uri} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,7 @@ type Props = {
|
||||||
includeSupportAction?: boolean,
|
includeSupportAction?: boolean,
|
||||||
hideActions?: boolean,
|
hideActions?: boolean,
|
||||||
renderActions?: Claim => ?Node,
|
renderActions?: Claim => ?Node,
|
||||||
|
wrapperElement?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
|
@ -95,7 +96,9 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
includeSupportAction,
|
includeSupportAction,
|
||||||
hideActions = false,
|
hideActions = false,
|
||||||
renderActions,
|
renderActions,
|
||||||
|
wrapperElement,
|
||||||
} = props;
|
} = props;
|
||||||
|
const WrapperElement = wrapperElement || 'li';
|
||||||
const shouldFetch =
|
const shouldFetch =
|
||||||
claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending);
|
claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending);
|
||||||
const abandoned = !isResolvingUri && !claim;
|
const abandoned = !isResolvingUri && !claim;
|
||||||
|
@ -221,7 +224,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<WrapperElement
|
||||||
ref={ref}
|
ref={ref}
|
||||||
role="link"
|
role="link"
|
||||||
onClick={pending || type === 'inline' ? undefined : handleOnClick}
|
onClick={pending || type === 'inline' ? undefined : handleOnClick}
|
||||||
|
@ -321,7 +324,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</WrapperElement>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -188,11 +188,11 @@ function Comment(props: Props) {
|
||||||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="comment__author-thumbnail">
|
<div className="comment__thumbnail-wrapper">
|
||||||
{authorUri ? (
|
{authorUri ? (
|
||||||
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small />
|
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small className="comment__author-thumbnail" />
|
||||||
) : (
|
) : (
|
||||||
<ChannelThumbnail small />
|
<ChannelThumbnail small className="comment__author-thumbnail" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -298,10 +298,10 @@ function Comment(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||||
<Expandable>
|
<Expandable>
|
||||||
<MarkdownPreview content={message} promptLinks />
|
<MarkdownPreview content={message} promptLinks parentCommentId={commentId} />
|
||||||
</Expandable>
|
</Expandable>
|
||||||
) : (
|
) : (
|
||||||
<MarkdownPreview content={message} promptLinks />
|
<MarkdownPreview content={message} promptLinks parentCommentId={commentId} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@ import {
|
||||||
selectCommentChannel,
|
selectCommentChannel,
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
import { doCommentList, doCommentReactList } from 'redux/actions/comments';
|
import { doCommentList, doCommentReactList } from 'redux/actions/comments';
|
||||||
import CommentsList from './view';
|
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
import CommentsList from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
myChannels: selectMyChannelClaims(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
|
|
|
@ -53,7 +53,9 @@ function CommentList(props: Props) {
|
||||||
const [end, setEnd] = React.useState(9);
|
const [end, setEnd] = React.useState(9);
|
||||||
// Display comments immediately if not fetching reactions
|
// Display comments immediately if not fetching reactions
|
||||||
// If not, wait to show comments until reactions are fetched
|
// If not, wait to show comments until reactions are fetched
|
||||||
const [readyToDisplayComments, setReadyToDisplayComments] = React.useState(!ENABLE_COMMENT_REACTIONS);
|
const [readyToDisplayComments, setReadyToDisplayComments] = React.useState(
|
||||||
|
reactionsById || !ENABLE_COMMENT_REACTIONS
|
||||||
|
);
|
||||||
const linkedCommentId = linkedComment && linkedComment.comment_id;
|
const linkedCommentId = linkedComment && linkedComment.comment_id;
|
||||||
const hasNoComments = totalComments === 0;
|
const hasNoComments = totalComments === 0;
|
||||||
const moreBelow = totalComments - end > 0;
|
const moreBelow = totalComments - end > 0;
|
||||||
|
@ -203,8 +205,7 @@ function CommentList(props: Props) {
|
||||||
{!isFetchingComments && hasNoComments && <div className="main--empty">{__('Be the first to comment!')}</div>}
|
{!isFetchingComments && hasNoComments && <div className="main--empty">{__('Be the first to comment!')}</div>}
|
||||||
|
|
||||||
<ul className="comments" ref={commentRef}>
|
<ul className="comments" ref={commentRef}>
|
||||||
{!isFetchingComments &&
|
{comments &&
|
||||||
comments &&
|
|
||||||
displayedComments &&
|
displayedComments &&
|
||||||
displayedComments.map(comment => {
|
displayedComments.map(comment => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -19,8 +19,13 @@ class LoadingScreen extends React.PureComponent<Props> {
|
||||||
const { status, spinner, isDocument } = this.props;
|
const { status, spinner, isDocument } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={classnames('content__loading', { 'content__loading--document': isDocument })}>
|
<div className={classnames('content__loading', { 'content__loading--document': isDocument })}>
|
||||||
{spinner && <Spinner light={!isDocument} />}
|
{spinner && (
|
||||||
{status && <span className={classnames('content__loading-text')}>{status}</span>}
|
<Spinner
|
||||||
|
light={!isDocument}
|
||||||
|
delayed
|
||||||
|
text={status && <span className={classnames('content__loading-text')}>{status}</span>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,9 @@ import remarkStrip from 'strip-markdown';
|
||||||
import remarkEmoji from 'remark-emoji';
|
import remarkEmoji from 'remark-emoji';
|
||||||
import remarkBreaks from 'remark-breaks';
|
import remarkBreaks from 'remark-breaks';
|
||||||
import reactRenderer from 'remark-react';
|
import reactRenderer from 'remark-react';
|
||||||
import ExternalLink from 'component/externalLink';
|
import MarkdownLink from 'component/markdownLink';
|
||||||
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';
|
import { formatedLinks, inlineLinks } from 'util/remark-lbry';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
|
||||||
import EmbedPlayButton from 'component/embedPlayButton';
|
|
||||||
|
|
||||||
type SimpleTextProps = {
|
type SimpleTextProps = {
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
|
@ -22,7 +19,6 @@ type SimpleLinkProps = {
|
||||||
href?: string,
|
href?: string,
|
||||||
title?: string,
|
title?: string,
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
noDataStore?: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type MarkdownProps = {
|
type MarkdownProps = {
|
||||||
|
@ -31,6 +27,8 @@ type MarkdownProps = {
|
||||||
promptLinks?: boolean,
|
promptLinks?: boolean,
|
||||||
noDataStore?: boolean,
|
noDataStore?: boolean,
|
||||||
className?: string,
|
className?: string,
|
||||||
|
parentCommentId?: string,
|
||||||
|
isMarkdownPost?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SimpleText = (props: SimpleTextProps) => {
|
const SimpleText = (props: SimpleTextProps) => {
|
||||||
|
@ -38,14 +36,13 @@ const SimpleText = (props: SimpleTextProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const SimpleLink = (props: SimpleLinkProps) => {
|
const SimpleLink = (props: SimpleLinkProps) => {
|
||||||
const { title, children } = props;
|
const { title, children, href } = props;
|
||||||
const { href, noDataStore } = props;
|
|
||||||
|
|
||||||
if (!href) {
|
if (!href) {
|
||||||
return children || null;
|
return children || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!href.startsWith('lbry://')) {
|
if (!href.startsWith('lbry:/')) {
|
||||||
return (
|
return (
|
||||||
<a href={href} title={title} target={'_blank'} rel={'noreferrer noopener'}>
|
<a href={href} title={title} target={'_blank'} rel={'noreferrer noopener'}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -60,33 +57,15 @@ const SimpleLink = (props: SimpleLinkProps) => {
|
||||||
if (embed) {
|
if (embed) {
|
||||||
// Decode this since users might just copy it from the url bar
|
// Decode this since users might just copy it from the url bar
|
||||||
const decodedUri = decodeURI(uri);
|
const decodedUri = decodeURI(uri);
|
||||||
return noDataStore ? (
|
return (
|
||||||
<div className="embed__inline-button-preview">
|
<div className="embed__inline-button-preview">
|
||||||
<pre>{decodedUri}</pre>
|
<pre>{decodedUri}</pre>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<EmbedPlayButton uri={decodedUri} />
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const webLink = formatLbryUrlForWeb(uri);
|
// Dummy link (no 'href')
|
||||||
// using Link after formatLbryUrl to handle "/" vs "#/"
|
return <a title={title}>{children}</a>;
|
||||||
// for web and desktop scenarios respectively
|
|
||||||
|
|
||||||
return noDataStore ? (
|
|
||||||
// Dummy link (no 'href')
|
|
||||||
<a title={title}>{children}</a>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
title={title}
|
|
||||||
to={webLink}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use github sanitation schema
|
// Use github sanitation schema
|
||||||
|
@ -99,7 +78,7 @@ schema.attributes.a.push('embed');
|
||||||
const REPLACE_REGEX = /(<iframe\s+src=["'])(.*?(?=))(["']\s*><\/iframe>)/g;
|
const REPLACE_REGEX = /(<iframe\s+src=["'])(.*?(?=))(["']\s*><\/iframe>)/g;
|
||||||
|
|
||||||
const MarkdownPreview = (props: MarkdownProps) => {
|
const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
const { content, strip, promptLinks, noDataStore, className } = props;
|
const { content, strip, promptLinks, noDataStore, className, parentCommentId, isMarkdownPost } = props;
|
||||||
const strippedContent = content
|
const strippedContent = content
|
||||||
? content.replace(REPLACE_REGEX, (iframeHtml, y, iframeSrc) => {
|
? content.replace(REPLACE_REGEX, (iframeHtml, y, iframeSrc) => {
|
||||||
// Let the browser try to create an iframe to see if the markup is valid
|
// Let the browser try to create an iframe to see if the markup is valid
|
||||||
|
@ -111,7 +90,7 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
const src = iframe.src;
|
const src = iframe.src;
|
||||||
|
|
||||||
if (src && src.startsWith('lbry://')) {
|
if (src && src.startsWith('lbry://')) {
|
||||||
return `${src}?embed=true`;
|
return src;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +102,16 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
sanitize: schema,
|
sanitize: schema,
|
||||||
fragment: React.Fragment,
|
fragment: React.Fragment,
|
||||||
remarkReactComponents: {
|
remarkReactComponents: {
|
||||||
a: promptLinks ? ExternalLink : linkProps => <SimpleLink {...linkProps} noDataStore={noDataStore} />,
|
a: noDataStore
|
||||||
|
? SimpleLink
|
||||||
|
: linkProps => (
|
||||||
|
<MarkdownLink
|
||||||
|
{...linkProps}
|
||||||
|
parentCommentId={parentCommentId}
|
||||||
|
isMarkdownPost={isMarkdownPost}
|
||||||
|
promptLinks={promptLinks}
|
||||||
|
/>
|
||||||
|
),
|
||||||
// Workaraund of remarkOptions.Fragment
|
// Workaraund of remarkOptions.Fragment
|
||||||
div: React.Fragment,
|
div: React.Fragment,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri, SETTINGS } from 'lbry-redux';
|
import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri, SETTINGS } from 'lbry-redux';
|
||||||
import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc';
|
import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { doSetFloatingUri, doPlayUri } from 'redux/actions/content';
|
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||||
|
|
||||||
import ChannelThumbnail from './view';
|
import ChannelThumbnail from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
|
@ -11,12 +13,13 @@ const select = (state, props) => ({
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
doFetchCostInfoForUri,
|
doFetchCostInfoForUri,
|
||||||
doSetFloatingUri,
|
|
||||||
doPlayUri,
|
doPlayUri,
|
||||||
|
doSetPlayingUri,
|
||||||
doAnaltyicsPurchaseEvent,
|
doAnaltyicsPurchaseEvent,
|
||||||
})(ChannelThumbnail);
|
})(ChannelThumbnail);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
@ -12,11 +14,14 @@ type Props = {
|
||||||
claim: ?Claim,
|
claim: ?Claim,
|
||||||
doResolveUri: string => void,
|
doResolveUri: string => void,
|
||||||
doFetchCostInfoForUri: string => void,
|
doFetchCostInfoForUri: string => void,
|
||||||
doSetFloatingUri: string => void,
|
|
||||||
costInfo: ?{ cost: number },
|
costInfo: ?{ cost: number },
|
||||||
floatingPlayerEnabled: boolean,
|
floatingPlayerEnabled: boolean,
|
||||||
doPlayUri: (string, ?boolean, ?boolean, (GetResponse) => void) => void,
|
doPlayUri: (string, ?boolean, ?boolean, (GetResponse) => void) => void,
|
||||||
doAnaltyicsPurchaseEvent: GetResponse => void,
|
doAnaltyicsPurchaseEvent: GetResponse => void,
|
||||||
|
parentCommentId?: string,
|
||||||
|
isMarkdownPost: boolean,
|
||||||
|
doSetPlayingUri: ({}) => void,
|
||||||
|
renderMode: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EmbedPlayButton(props: Props) {
|
export default function EmbedPlayButton(props: Props) {
|
||||||
|
@ -26,33 +31,54 @@ export default function EmbedPlayButton(props: Props) {
|
||||||
claim,
|
claim,
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
doFetchCostInfoForUri,
|
doFetchCostInfoForUri,
|
||||||
doSetFloatingUri,
|
|
||||||
floatingPlayerEnabled,
|
floatingPlayerEnabled,
|
||||||
doPlayUri,
|
doPlayUri,
|
||||||
|
doSetPlayingUri,
|
||||||
doAnaltyicsPurchaseEvent,
|
doAnaltyicsPurchaseEvent,
|
||||||
costInfo,
|
costInfo,
|
||||||
|
parentCommentId,
|
||||||
|
isMarkdownPost,
|
||||||
|
renderMode,
|
||||||
} = props;
|
} = props;
|
||||||
const { push } = useHistory();
|
const {
|
||||||
|
push,
|
||||||
|
location: { pathname },
|
||||||
|
} = useHistory();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const hasResolvedUri = claim !== undefined;
|
const hasResolvedUri = claim !== undefined;
|
||||||
|
const hasCostInfo = costInfo !== undefined;
|
||||||
const disabled = !hasResolvedUri || !costInfo;
|
const disabled = !hasResolvedUri || !costInfo;
|
||||||
|
const canPlayInline = [RENDER_MODES.AUDIO, RENDER_MODES.VIDEO].includes(renderMode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
doResolveUri(uri);
|
if (!hasResolvedUri) {
|
||||||
doFetchCostInfoForUri(uri);
|
doResolveUri(uri);
|
||||||
}, [uri, doResolveUri, doFetchCostInfoForUri]);
|
}
|
||||||
|
|
||||||
|
if (!hasCostInfo) {
|
||||||
|
doFetchCostInfoForUri(uri);
|
||||||
|
}
|
||||||
|
}, [uri, doResolveUri, doFetchCostInfoForUri, hasCostInfo, hasResolvedUri]);
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile || !floatingPlayerEnabled) {
|
if (isMobile || !floatingPlayerEnabled || !canPlayInline) {
|
||||||
const formattedUrl = formatLbryUrlForWeb(uri);
|
const formattedUrl = formatLbryUrlForWeb(uri);
|
||||||
push(formattedUrl);
|
push(formattedUrl);
|
||||||
} else {
|
} else {
|
||||||
doPlayUri(uri, undefined, undefined, fileInfo => {
|
doPlayUri(uri, undefined, undefined, fileInfo => {
|
||||||
doSetFloatingUri(uri);
|
let playingOptions: PlayingUri = { uri, pathname };
|
||||||
|
if (parentCommentId) {
|
||||||
|
playingOptions.source = 'comment';
|
||||||
|
playingOptions.commentId = parentCommentId;
|
||||||
|
} else if (isMarkdownPost) {
|
||||||
|
playingOptions.source = 'markdown';
|
||||||
|
}
|
||||||
|
|
||||||
|
doSetPlayingUri(playingOptions);
|
||||||
doAnaltyicsPurchaseEvent(fileInfo);
|
doAnaltyicsPurchaseEvent(fileInfo);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -67,7 +93,16 @@ export default function EmbedPlayButton(props: Props) {
|
||||||
style={{ backgroundImage: `url('${thumbnail.replace(/'/g, "\\'")}')` }}
|
style={{ backgroundImage: `url('${thumbnail.replace(/'/g, "\\'")}')` }}
|
||||||
>
|
>
|
||||||
<FileViewerEmbeddedTitle uri={uri} isInApp />
|
<FileViewerEmbeddedTitle uri={uri} isInApp />
|
||||||
<Button onClick={handleClick} iconSize={30} title={__('Play')} className={'button--icon button--play'} />
|
<Button
|
||||||
|
onClick={handleClick}
|
||||||
|
iconSize={30}
|
||||||
|
title={__('Play')}
|
||||||
|
className={classnames('button--icon', {
|
||||||
|
'button--play': canPlayInline,
|
||||||
|
'button--view': !canPlayInline,
|
||||||
|
})}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as MODALS from 'constants/modal_types';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import * as React from 'react';
|
|
||||||
import { isURIValid } from 'lbry-redux';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import ClaimLink from 'component/claimLink';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
href: string,
|
|
||||||
title?: string,
|
|
||||||
embed?: boolean,
|
|
||||||
children: React.Node,
|
|
||||||
openModal: (id: string, { uri: string }) => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
class ExternalLink extends React.PureComponent<Props> {
|
|
||||||
static defaultProps = {
|
|
||||||
href: null,
|
|
||||||
title: null,
|
|
||||||
embed: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
createLink() {
|
|
||||||
const { href, title, embed, children, openModal } = this.props;
|
|
||||||
// Regex for url protocol
|
|
||||||
const protocolRegex = new RegExp('^(https?|lbry|mailto)+:', 'i');
|
|
||||||
const protocol = href ? protocolRegex.exec(href) : null;
|
|
||||||
// Return plain text if no valid url
|
|
||||||
let element = <span>{children}</span>;
|
|
||||||
// Return external link if protocol is http or https
|
|
||||||
if (protocol && (protocol[0] === 'http:' || protocol[0] === 'https:' || protocol[0] === 'mailto:')) {
|
|
||||||
element = (
|
|
||||||
<Button
|
|
||||||
button="link"
|
|
||||||
iconRight={ICONS.EXTERNAL}
|
|
||||||
title={title || href}
|
|
||||||
label={children}
|
|
||||||
className="button--external-link"
|
|
||||||
onClick={() => {
|
|
||||||
openModal(MODALS.CONFIRM_EXTERNAL_RESOURCE, { uri: href, isTrusted: false });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Return local link if protocol is lbry uri
|
|
||||||
if (protocol && protocol[0] === 'lbry:' && isURIValid(href)) {
|
|
||||||
element = (
|
|
||||||
<ClaimLink uri={href} autoEmbed={embed}>
|
|
||||||
{children}
|
|
||||||
</ClaimLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const RenderLink = () => this.createLink();
|
|
||||||
return <RenderLink />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExternalLink;
|
|
|
@ -150,6 +150,7 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
className={classnames('file-render', className, {
|
className={classnames('file-render', className, {
|
||||||
'file-render--document': RENDER_MODES.TEXT_MODES.includes(renderMode) && !embedded,
|
'file-render--document': RENDER_MODES.TEXT_MODES.includes(renderMode) && !embedded,
|
||||||
'file-render--embed': embedded,
|
'file-render--embed': embedded,
|
||||||
|
'file-render--video': renderMode === RENDER_MODES.VIDEO || renderMode === RENDER_MODES.AUDIO,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{this.renderViewer()}
|
{this.renderViewer()}
|
||||||
|
|
|
@ -2,21 +2,24 @@ import { connect } from 'react-redux';
|
||||||
import { makeSelectFileInfoForUri, makeSelectTitleForUri, makeSelectStreamingUrlForUri, SETTINGS } from 'lbry-redux';
|
import { makeSelectFileInfoForUri, makeSelectTitleForUri, makeSelectStreamingUrlForUri, SETTINGS } from 'lbry-redux';
|
||||||
import {
|
import {
|
||||||
makeSelectIsPlayerFloating,
|
makeSelectIsPlayerFloating,
|
||||||
selectFloatingUri,
|
selectPrimaryUri,
|
||||||
selectPlayingUri,
|
selectPlayingUri,
|
||||||
makeSelectFileRenderModeForUri,
|
makeSelectFileRenderModeForUri,
|
||||||
} from 'redux/selectors/content';
|
} from 'redux/selectors/content';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { doCloseFloatingPlayer } from 'redux/actions/content';
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import FileRenderFloating from './view';
|
import FileRenderFloating from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const floatingUri = selectFloatingUri(state);
|
|
||||||
const playingUri = selectPlayingUri(state);
|
const playingUri = selectPlayingUri(state);
|
||||||
const uri = floatingUri || playingUri;
|
const primaryUri = selectPrimaryUri(state);
|
||||||
|
const uri = playingUri && playingUri.uri;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uri,
|
uri,
|
||||||
|
primaryUri,
|
||||||
|
playingUri,
|
||||||
title: makeSelectTitleForUri(uri)(state),
|
title: makeSelectTitleForUri(uri)(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(uri)(state),
|
fileInfo: makeSelectFileInfoForUri(uri)(state),
|
||||||
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
||||||
|
@ -27,7 +30,7 @@ const select = (state, props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
closeFloatingPlayer: () => dispatch(doCloseFloatingPlayer(null)),
|
closeFloatingPlayer: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(FileRenderFloating));
|
export default withRouter(connect(select, perform)(FileRenderFloating));
|
||||||
|
|
|
@ -8,14 +8,16 @@ import LoadingScreen from 'component/common/loading-screen';
|
||||||
import FileRender from 'component/fileRender';
|
import FileRender from 'component/fileRender';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import { FILE_WRAPPER_CLASS } from 'page/file/view';
|
import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
|
||||||
import Draggable from 'react-draggable';
|
import Draggable from 'react-draggable';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
import { onFullscreenChange } from 'util/full-screen';
|
import { onFullscreenChange } from 'util/full-screen';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
|
||||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
|
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
|
||||||
|
export const INLINE_PLAYER_WRAPPER_CLASS = 'inline-player__wrapper';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isFloating: boolean,
|
isFloating: boolean,
|
||||||
|
@ -26,6 +28,8 @@ type Props = {
|
||||||
floatingPlayerEnabled: boolean,
|
floatingPlayerEnabled: boolean,
|
||||||
closeFloatingPlayer: () => void,
|
closeFloatingPlayer: () => void,
|
||||||
renderMode: string,
|
renderMode: string,
|
||||||
|
playingUri: ?PlayingUri,
|
||||||
|
primaryUri: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileRenderFloating(props: Props) {
|
export default function FileRenderFloating(props: Props) {
|
||||||
|
@ -38,9 +42,14 @@ export default function FileRenderFloating(props: Props) {
|
||||||
closeFloatingPlayer,
|
closeFloatingPlayer,
|
||||||
floatingPlayerEnabled,
|
floatingPlayerEnabled,
|
||||||
renderMode,
|
renderMode,
|
||||||
|
playingUri,
|
||||||
|
primaryUri,
|
||||||
} = props;
|
} = props;
|
||||||
|
const {
|
||||||
|
location: { pathname },
|
||||||
|
} = useHistory();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const mainFilePlaying = playingUri && playingUri.uri === primaryUri;
|
||||||
const [fileViewerRect, setFileViewerRect] = useState();
|
const [fileViewerRect, setFileViewerRect] = useState();
|
||||||
const [desktopPlayStartTime, setDesktopPlayStartTime] = useState();
|
const [desktopPlayStartTime, setDesktopPlayStartTime] = useState();
|
||||||
const [wasDragging, setWasDragging] = useState(false);
|
const [wasDragging, setWasDragging] = useState(false);
|
||||||
|
@ -48,8 +57,12 @@ export default function FileRenderFloating(props: Props) {
|
||||||
x: -25,
|
x: -25,
|
||||||
y: window.innerHeight - 400,
|
y: window.innerHeight - 400,
|
||||||
});
|
});
|
||||||
const [relativePos, setRelativePos] = useState({ x: 0, y: 0 });
|
const [relativePos, setRelativePos] = useState({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const playingUriSource = playingUri && playingUri.source;
|
||||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||||
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
|
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
|
||||||
const loadingMessage =
|
const loadingMessage =
|
||||||
|
@ -98,15 +111,18 @@ export default function FileRenderFloating(props: Props) {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Ensure player is within screen when 'isFloating' changes.
|
// Ensure player is within screen when 'isFloating' changes.
|
||||||
|
const stringifiedPosition = JSON.stringify(position);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const jsonPosition = JSON.parse(stringifiedPosition);
|
||||||
|
|
||||||
if (isFloating) {
|
if (isFloating) {
|
||||||
let pos = { x: position.x, y: position.y };
|
let pos = { x: jsonPosition.x, y: jsonPosition.y };
|
||||||
clampToScreen(pos);
|
clampToScreen(pos);
|
||||||
if (pos.x !== position.x || pos.y !== position.y) {
|
if (pos.x !== position.x || pos.y !== position.y) {
|
||||||
setPosition({ x: pos.x, y: pos.y });
|
setPosition({ x: pos.x, y: pos.y });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isFloating]);
|
}, [isFloating, stringifiedPosition]);
|
||||||
|
|
||||||
// Listen to main-window resizing and adjust the fp position accordingly:
|
// Listen to main-window resizing and adjust the fp position accordingly:
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -126,27 +142,37 @@ export default function FileRenderFloating(props: Props) {
|
||||||
// Otherwise, this could just be changed to a one-time effect.
|
// Otherwise, this could just be changed to a one-time effect.
|
||||||
}, [relativePos]);
|
}, [relativePos]);
|
||||||
|
|
||||||
// Update 'fileViewerRect':
|
function handleResize() {
|
||||||
useEffect(() => {
|
const element = mainFilePlaying
|
||||||
function handleResize() {
|
? document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`)
|
||||||
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
|
: document.querySelector(`.${INLINE_PLAYER_WRAPPER_CLASS}`);
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = element.getBoundingClientRect();
|
if (!element) {
|
||||||
// $FlowFixMe
|
return;
|
||||||
setFileViewerRect(rect);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
setFileViewerRect(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (streamingUrl) {
|
||||||
|
handleResize();
|
||||||
|
}
|
||||||
|
}, [streamingUrl, pathname, playingUriSource, isFloating, mainFilePlaying]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
handleResize();
|
handleResize();
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
onFullscreenChange(window, 'add', handleResize);
|
onFullscreenChange(window, 'add', handleResize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
onFullscreenChange(window, 'remove', handleResize);
|
onFullscreenChange(window, 'remove', handleResize);
|
||||||
};
|
};
|
||||||
}, [setFileViewerRect, isFloating]);
|
}, [setFileViewerRect, isFloating, playingUriSource, mainFilePlaying]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
|
@ -210,7 +236,13 @@ export default function FileRenderFloating(props: Props) {
|
||||||
})}
|
})}
|
||||||
style={
|
style={
|
||||||
!isFloating && fileViewerRect
|
!isFloating && fileViewerRect
|
||||||
? { width: fileViewerRect.width, height: fileViewerRect.height, left: fileViewerRect.x }
|
? {
|
||||||
|
width: fileViewerRect.width,
|
||||||
|
height: fileViewerRect.height,
|
||||||
|
left: fileViewerRect.x,
|
||||||
|
// 80px is header height in scss/init/vars.scss
|
||||||
|
top: window.pageYOffset + fileViewerRect.top - 80,
|
||||||
|
}
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
import { doPlayUri, doSetPlayingUri, doSetPrimaryUri } from 'redux/actions/content';
|
||||||
import {
|
import {
|
||||||
makeSelectFileInfoForUri,
|
makeSelectFileInfoForUri,
|
||||||
makeSelectThumbnailForUri,
|
makeSelectThumbnailForUri,
|
||||||
|
@ -41,7 +41,8 @@ const select = (state, props) => ({
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
play: uri => {
|
play: uri => {
|
||||||
dispatch(doSetPlayingUri(uri));
|
dispatch(doSetPrimaryUri(uri));
|
||||||
|
dispatch(doSetPlayingUri({ uri }));
|
||||||
dispatch(doPlayUri(uri, undefined, undefined, fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
|
dispatch(doPlayUri(uri, undefined, undefined, fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,7 +52,6 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
claimWasPurchased,
|
claimWasPurchased,
|
||||||
authenticated,
|
authenticated,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const cost = costInfo && costInfo.cost;
|
const cost = costInfo && costInfo.cost;
|
||||||
const isFree = hasCostInfo && cost === 0;
|
const isFree = hasCostInfo && cost === 0;
|
||||||
const fileStatus = fileInfo && fileInfo.status;
|
const fileStatus = fileInfo && fileInfo.status;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'lbry-redux';
|
import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'lbry-redux';
|
||||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||||
import { makeSelectFileRenderModeForUri, makeSelectIsPlaying } from 'redux/selectors/content';
|
import { makeSelectFileRenderModeForUri, selectPrimaryUri } from 'redux/selectors/content';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doAnalyticsView } from 'redux/actions/app';
|
import { doAnalyticsView } from 'redux/actions/app';
|
||||||
import FileRenderInline from './view';
|
import FileRenderInline from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
isPlaying: selectPrimaryUri(state) === props.uri,
|
||||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,6 +27,7 @@ function FileViewerEmbeddedTitle(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-viewer__embedded-header">
|
<div className="file-viewer__embedded-header">
|
||||||
|
<div className="file-viewer__embedded-gradient" />
|
||||||
<Button label={title} button="link" className="file-viewer__embedded-title" {...contentLinkProps} />
|
<Button label={title} button="link" className="file-viewer__embedded-title" {...contentLinkProps} />
|
||||||
<div className="file-viewer__embedded-info">
|
<div className="file-viewer__embedded-info">
|
||||||
<Button className="file-viewer__overlay-logo" icon={ICONS.LBRY} {...lbryLinkProps} />
|
<Button className="file-viewer__overlay-logo" icon={ICONS.LBRY} {...lbryLinkProps} />
|
||||||
|
|
|
@ -7,7 +7,4 @@ const perform = dispatch => ({
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(ExternalLink);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(ExternalLink);
|
|
79
ui/component/markdownLink/view.jsx
Normal file
79
ui/component/markdownLink/view.jsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// @flow
|
||||||
|
import { KNOWN_APP_DOMAINS } from 'config';
|
||||||
|
import * as MODALS from 'constants/modal_types';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { isURIValid } from 'lbry-redux';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import ClaimLink from 'component/claimLink';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
href: string,
|
||||||
|
title?: string,
|
||||||
|
embed?: boolean,
|
||||||
|
children: React.Node,
|
||||||
|
openModal: (id: string, { uri: string }) => void,
|
||||||
|
parentCommentId?: string,
|
||||||
|
isMarkdownPost?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
function MarkdownLink(props: Props) {
|
||||||
|
const { children, href, title, embed = false, openModal, parentCommentId, isMarkdownPost } = props;
|
||||||
|
const decodedUri = decodeURI(href);
|
||||||
|
if (!href) {
|
||||||
|
return children || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex for url protocol
|
||||||
|
const protocolRegex = new RegExp('^(https?|lbry|mailto)+:', 'i');
|
||||||
|
const protocol = href ? protocolRegex.exec(href) : null;
|
||||||
|
// Return plain text if no valid url
|
||||||
|
let element = <span>{children}</span>;
|
||||||
|
// Return external link if protocol is http or https
|
||||||
|
|
||||||
|
const linkUrlObject = new URL(decodedUri);
|
||||||
|
const linkDomain = linkUrlObject.host;
|
||||||
|
const isKnownAppDomainLink = KNOWN_APP_DOMAINS.includes(linkDomain);
|
||||||
|
let lbryUrlFromLink;
|
||||||
|
if (isKnownAppDomainLink) {
|
||||||
|
const linkPathname = decodeURIComponent(
|
||||||
|
linkUrlObject.pathname.startsWith('//') ? linkUrlObject.pathname.slice(2) : linkUrlObject.pathname.slice(1)
|
||||||
|
);
|
||||||
|
const possibleLbryUrl = `lbry://${linkPathname.replace(/:/g, '#')}`;
|
||||||
|
const lbryLinkIsValid = isURIValid(possibleLbryUrl);
|
||||||
|
if (lbryLinkIsValid) {
|
||||||
|
lbryUrlFromLink = possibleLbryUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return local link if protocol is lbry uri
|
||||||
|
if ((protocol && protocol[0] === 'lbry:' && isURIValid(decodedUri)) || lbryUrlFromLink) {
|
||||||
|
element = (
|
||||||
|
<ClaimLink
|
||||||
|
uri={lbryUrlFromLink || decodedUri}
|
||||||
|
autoEmbed={embed}
|
||||||
|
parentCommentId={parentCommentId}
|
||||||
|
isMarkdownPost={isMarkdownPost}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ClaimLink>
|
||||||
|
);
|
||||||
|
} else if (protocol && (protocol[0] === 'http:' || protocol[0] === 'https:' || protocol[0] === 'mailto:')) {
|
||||||
|
element = (
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
iconRight={ICONS.EXTERNAL}
|
||||||
|
title={title || decodedUri}
|
||||||
|
label={children}
|
||||||
|
className="button--external-link"
|
||||||
|
onClick={() => {
|
||||||
|
openModal(MODALS.CONFIRM_EXTERNAL_RESOURCE, { uri: href, isTrusted: false });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{element}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MarkdownLink;
|
|
@ -5,6 +5,8 @@ import TruncatedText from 'component/common/truncated-text';
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
import { parseURI } from 'lbry-redux';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -22,6 +24,7 @@ class PreviewLink extends React.PureComponent<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { uri, title, description, thumbnail } = this.props;
|
const { uri, title, description, thumbnail } = this.props;
|
||||||
|
const { isChannel } = parseURI(uri);
|
||||||
const placeholder = 'static/img/placeholder.png';
|
const placeholder = 'static/img/placeholder.png';
|
||||||
|
|
||||||
const thumbnailStyle = {
|
const thumbnailStyle = {
|
||||||
|
@ -31,7 +34,12 @@ class PreviewLink extends React.PureComponent<Props> {
|
||||||
return (
|
return (
|
||||||
<span className="preview-link" role="button" onClick={this.handleClick}>
|
<span className="preview-link" role="button" onClick={this.handleClick}>
|
||||||
<span className="claim-preview">
|
<span className="claim-preview">
|
||||||
<span style={thumbnailStyle} className="preview-link__thumbnail media__thumb" />
|
<span
|
||||||
|
style={thumbnailStyle}
|
||||||
|
className={classnames('preview-link__thumbnail media__thumb', {
|
||||||
|
'preview-link__thumbnail--channel': isChannel,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<span className="claim-preview-metadata">
|
<span className="claim-preview-metadata">
|
||||||
<span className="claim-preview-info">
|
<span className="claim-preview-info">
|
||||||
<span className="claim-preview__title">
|
<span className="claim-preview__title">
|
||||||
|
|
|
@ -9,6 +9,7 @@ type Props = {
|
||||||
theme: string,
|
theme: string,
|
||||||
type: ?string,
|
type: ?string,
|
||||||
delayed: boolean,
|
delayed: boolean,
|
||||||
|
text?: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -54,7 +55,7 @@ class Spinner extends PureComponent<Props, State> {
|
||||||
delayedTimeout: ?TimeoutID;
|
delayedTimeout: ?TimeoutID;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dark, light, theme, type } = this.props;
|
const { dark, light, theme, type, text } = this.props;
|
||||||
const { show } = this.state;
|
const { show } = this.state;
|
||||||
|
|
||||||
if (!show) {
|
if (!show) {
|
||||||
|
@ -62,19 +63,22 @@ class Spinner extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={classnames('spinner', {
|
{text}
|
||||||
'spinner--dark': !light && (dark || theme === LIGHT_THEME),
|
<div
|
||||||
'spinner--light': !dark && (light || theme === DARK_THEME),
|
className={classnames('spinner', {
|
||||||
'spinner--small': type === 'small',
|
'spinner--dark': !light && (dark || theme === LIGHT_THEME),
|
||||||
})}
|
'spinner--light': !dark && (light || theme === DARK_THEME),
|
||||||
>
|
'spinner--small': type === 'small',
|
||||||
<div className="rect rect1" />
|
})}
|
||||||
<div className="rect rect2" />
|
>
|
||||||
<div className="rect rect3" />
|
<div className="rect rect1" />
|
||||||
<div className="rect rect4" />
|
<div className="rect rect2" />
|
||||||
<div className="rect rect5" />
|
<div className="rect rect3" />
|
||||||
</div>
|
<div className="rect rect4" />
|
||||||
|
<div className="rect rect5" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
|
||||||
const { contentType } = source;
|
const { contentType } = source;
|
||||||
|
|
||||||
return renderMode === RENDER_MODES.MARKDOWN ? (
|
return renderMode === RENDER_MODES.MARKDOWN ? (
|
||||||
<Card body={<MarkdownPreview content={content} />} />
|
<Card body={<MarkdownPreview content={content} isMarkdownPost promptLinks />} />
|
||||||
) : (
|
) : (
|
||||||
<CodeViewer value={content} contentType={contentType} theme={theme} />
|
<CodeViewer value={content} contentType={contentType} theme={theme} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -88,8 +88,8 @@ export const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED';
|
||||||
export const PUBLISH_STARTED = 'PUBLISH_STARTED';
|
export const PUBLISH_STARTED = 'PUBLISH_STARTED';
|
||||||
export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED';
|
export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED';
|
||||||
export const PUBLISH_FAILED = 'PUBLISH_FAILED';
|
export const PUBLISH_FAILED = 'PUBLISH_FAILED';
|
||||||
|
export const SET_PRIMARY_URI = 'SET_PRIMARY_URI';
|
||||||
export const SET_PLAYING_URI = 'SET_PLAYING_URI';
|
export const SET_PLAYING_URI = 'SET_PLAYING_URI';
|
||||||
export const SET_FLOATING_URI = 'SET_FLOATING_URI';
|
|
||||||
export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION';
|
export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION';
|
||||||
export const CLEAR_CONTENT_POSITION = 'CLEAR_CONTENT_POSITION';
|
export const CLEAR_CONTENT_POSITION = 'CLEAR_CONTENT_POSITION';
|
||||||
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
|
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doPlayUri, doSetPlayingUri, doSetFloatingUri } from 'redux/actions/content';
|
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { selectPlayingUri } from 'redux/selectors/content';
|
import { selectPlayingUri } from 'redux/selectors/content';
|
||||||
import { doHideModal, doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
import { doHideModal, doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
||||||
import { makeSelectMetadataForUri } from 'lbry-redux';
|
import { makeSelectMetadataForUri } from 'lbry-redux';
|
||||||
|
@ -12,8 +12,7 @@ const select = (state, props) => ({
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
analyticsPurchaseEvent: fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo)),
|
analyticsPurchaseEvent: fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo)),
|
||||||
setPlayingUri: uri => dispatch(doSetPlayingUri(uri)),
|
setPlayingUri: uri => dispatch(doSetPlayingUri({ uri })),
|
||||||
setFloatingUri: uri => dispatch(doSetFloatingUri(uri)),
|
|
||||||
closeModal: () => dispatch(doHideModal()),
|
closeModal: () => dispatch(doHideModal()),
|
||||||
loadVideo: (uri, onSuccess) => dispatch(doPlayUri(uri, true, undefined, onSuccess)),
|
loadVideo: (uri, onSuccess) => dispatch(doPlayUri(uri, true, undefined, onSuccess)),
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,9 +17,8 @@ type Props = {
|
||||||
cancelPurchase: () => void,
|
cancelPurchase: () => void,
|
||||||
metadata: StreamMetadata,
|
metadata: StreamMetadata,
|
||||||
analyticsPurchaseEvent: GetResponse => void,
|
analyticsPurchaseEvent: GetResponse => void,
|
||||||
playingUri: ?string,
|
playingUri: ?PlayingUri,
|
||||||
setPlayingUri: (?string) => void,
|
setPlayingUri: (?string) => void,
|
||||||
setFloatingUri: (?string) => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ModalAffirmPurchase(props: Props) {
|
function ModalAffirmPurchase(props: Props) {
|
||||||
|
@ -31,11 +30,9 @@ function ModalAffirmPurchase(props: Props) {
|
||||||
analyticsPurchaseEvent,
|
analyticsPurchaseEvent,
|
||||||
playingUri,
|
playingUri,
|
||||||
setPlayingUri,
|
setPlayingUri,
|
||||||
setFloatingUri,
|
|
||||||
} = props;
|
} = props;
|
||||||
const [success, setSuccess] = React.useState(false);
|
const [success, setSuccess] = React.useState(false);
|
||||||
const [purchasing, setPurchasing] = React.useState(false);
|
const [purchasing, setPurchasing] = React.useState(false);
|
||||||
|
|
||||||
const modalTitle = __('Confirm Purchase');
|
const modalTitle = __('Confirm Purchase');
|
||||||
|
|
||||||
function onAffirmPurchase() {
|
function onAffirmPurchase() {
|
||||||
|
@ -45,14 +42,14 @@ function ModalAffirmPurchase(props: Props) {
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
analyticsPurchaseEvent(fileInfo);
|
analyticsPurchaseEvent(fileInfo);
|
||||||
|
|
||||||
if (playingUri !== uri) {
|
if (!playingUri || playingUri.uri !== uri) {
|
||||||
setFloatingUri(uri);
|
setPlayingUri(uri);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPurchase() {
|
function cancelPurchase() {
|
||||||
if (uri === playingUri) {
|
if (playingUri && uri === playingUri.uri) {
|
||||||
setPlayingUri(null);
|
setPlayingUri(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -201,7 +201,7 @@ function ChannelPage(props: Props) {
|
||||||
<TabList className="tabs__list--channel-page">
|
<TabList className="tabs__list--channel-page">
|
||||||
<Tab disabled={editing}>{__('Content')}</Tab>
|
<Tab disabled={editing}>{__('Content')}</Tab>
|
||||||
<Tab>{editing ? __('Editing Your Channel') : __('About --[tab title in Channel Page]--')}</Tab>
|
<Tab>{editing ? __('Editing Your Channel') : __('About --[tab title in Channel Page]--')}</Tab>
|
||||||
<Tab disabled={editing}>{__('Discussion')}</Tab>
|
<Tab disabled={editing}>{__('Community')}</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
|
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
|
||||||
import { doSetContentHistoryItem } from 'redux/actions/content';
|
import { doSetContentHistoryItem, doSetPrimaryUri } from 'redux/actions/content';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import {
|
import {
|
||||||
doFetchFileInfo,
|
doFetchFileInfo,
|
||||||
|
@ -39,6 +39,7 @@ const perform = dispatch => ({
|
||||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||||
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
|
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
|
||||||
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
||||||
|
setPrimaryUri: uri => dispatch(doSetPrimaryUri(uri)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(FilePage));
|
export default withRouter(connect(select, perform)(FilePage));
|
||||||
|
|
|
@ -12,7 +12,7 @@ import FileValues from 'component/fileValues';
|
||||||
import RecommendedContent from 'component/recommendedContent';
|
import RecommendedContent from 'component/recommendedContent';
|
||||||
import CommentsList from 'component/commentsList';
|
import CommentsList from 'component/commentsList';
|
||||||
|
|
||||||
export const FILE_WRAPPER_CLASS = 'file-page__video-container';
|
export const PRIMARY_PLAYER_WRAPPER_CLASS = 'file-page__video-container';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
costInfo: ?{ includesData: boolean, cost: number },
|
costInfo: ?{ includesData: boolean, cost: number },
|
||||||
|
@ -28,63 +28,61 @@ type Props = {
|
||||||
obscureNsfw: boolean,
|
obscureNsfw: boolean,
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
linkedComment: any,
|
linkedComment: any,
|
||||||
|
setPrimaryUri: (?string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FilePage extends React.Component<Props> {
|
function FilePage(props: Props) {
|
||||||
constructor() {
|
const {
|
||||||
super();
|
uri,
|
||||||
this.lastReset = undefined;
|
channelUri,
|
||||||
}
|
renderMode,
|
||||||
|
fetchFileInfo,
|
||||||
componentDidMount() {
|
fetchCostInfo,
|
||||||
const { uri, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed } = this.props;
|
setViewed,
|
||||||
|
isSubscribed,
|
||||||
if (isSubscribed) {
|
fileInfo,
|
||||||
this.removeFromSubscriptionNotifications();
|
markSubscriptionRead,
|
||||||
}
|
obscureNsfw,
|
||||||
|
isMature,
|
||||||
|
costInfo,
|
||||||
|
linkedComment,
|
||||||
|
setPrimaryUri,
|
||||||
|
} = props;
|
||||||
|
const cost = costInfo ? costInfo.cost : null;
|
||||||
|
const hasFileInfo = fileInfo !== undefined;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
// always refresh file info when entering file page to see if we have the file
|
// always refresh file info when entering file page to see if we have the file
|
||||||
// this could probably be refactored into more direct components now
|
// this could probably be refactored into more direct components now
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
fetchFileInfo(uri);
|
if (!hasFileInfo) {
|
||||||
|
fetchFileInfo(uri);
|
||||||
|
}
|
||||||
// @endif
|
// @endif
|
||||||
|
|
||||||
// See https://github.com/lbryio/lbry-desktop/pull/1563 for discussion
|
// See https://github.com/lbryio/lbry-desktop/pull/1563 for discussion
|
||||||
fetchCostInfo(uri);
|
fetchCostInfo(uri);
|
||||||
setViewed(uri);
|
setViewed(uri);
|
||||||
}
|
setPrimaryUri(uri);
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
return () => {
|
||||||
const { isSubscribed, uri, fileInfo, setViewed, fetchFileInfo } = this.props;
|
setPrimaryUri(null);
|
||||||
|
};
|
||||||
|
}, [uri, hasFileInfo, fetchFileInfo, fetchCostInfo, setViewed, setPrimaryUri]);
|
||||||
|
|
||||||
if (!prevProps.isSubscribed && isSubscribed) {
|
React.useEffect(() => {
|
||||||
this.removeFromSubscriptionNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.uri !== uri) {
|
|
||||||
setViewed(uri);
|
|
||||||
this.lastReset = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// @if TARGET='app'
|
|
||||||
if (prevProps.uri !== uri && fileInfo === undefined) {
|
|
||||||
fetchFileInfo(uri);
|
|
||||||
}
|
|
||||||
// @endif
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromSubscriptionNotifications() {
|
|
||||||
// Always try to remove
|
// Always try to remove
|
||||||
// If it doesn't exist, nothing will happen
|
// If it doesn't exist, nothing will happen
|
||||||
const { markSubscriptionRead, uri, channelUri } = this.props;
|
if (isSubscribed) {
|
||||||
markSubscriptionRead(channelUri, uri);
|
markSubscriptionRead(channelUri, uri);
|
||||||
}
|
}
|
||||||
|
}, [isSubscribed, markSubscriptionRead, uri, channelUri]);
|
||||||
|
|
||||||
renderFilePageLayout(uri: string, mode: string, cost: ?number) {
|
function renderFilePageLayout() {
|
||||||
if (RENDER_MODES.FLOATING_MODES.includes(mode)) {
|
if (RENDER_MODES.FLOATING_MODES.includes(renderMode)) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className={FILE_WRAPPER_CLASS}>
|
<div className={PRIMARY_PLAYER_WRAPPER_CLASS}>
|
||||||
<FileRenderInitiator uri={uri} />
|
<FileRenderInitiator uri={uri} />
|
||||||
</div>
|
</div>
|
||||||
{/* playables will be rendered and injected by <FileRenderFloating> */}
|
{/* playables will be rendered and injected by <FileRenderFloating> */}
|
||||||
|
@ -93,7 +91,7 @@ class FilePage extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RENDER_MODES.UNRENDERABLE_MODES.includes(mode)) {
|
if (RENDER_MODES.UNRENDERABLE_MODES.includes(renderMode)) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FileTitle uri={uri} />
|
<FileTitle uri={uri} />
|
||||||
|
@ -102,7 +100,7 @@ class FilePage extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RENDER_MODES.TEXT_MODES.includes(mode)) {
|
if (RENDER_MODES.TEXT_MODES.includes(renderMode)) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FileTitle uri={uri} />
|
<FileTitle uri={uri} />
|
||||||
|
@ -121,8 +119,7 @@ class FilePage extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBlockedPage() {
|
function renderBlockedPage() {
|
||||||
const { uri } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<FileTitle uri={uri} isNsfwBlocked />
|
<FileTitle uri={uri} isNsfwBlocked />
|
||||||
|
@ -130,29 +127,23 @@ class FilePage extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastReset: ?any;
|
if (obscureNsfw && isMature) {
|
||||||
|
return renderBlockedPage();
|
||||||
render() {
|
|
||||||
const { uri, renderMode, costInfo, obscureNsfw, isMature, linkedComment } = this.props;
|
|
||||||
|
|
||||||
if (obscureNsfw && isMature) {
|
|
||||||
return this.renderBlockedPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page className="file-page" filePage>
|
|
||||||
<div className={classnames('section card-stack', `file-page__${renderMode}`)}>
|
|
||||||
{this.renderFilePageLayout(uri, renderMode, costInfo ? costInfo.cost : null)}
|
|
||||||
<FileValues uri={uri} />
|
|
||||||
<FileDetails uri={uri} />
|
|
||||||
|
|
||||||
<CommentsList uri={uri} linkedComment={linkedComment} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RecommendedContent uri={uri} />
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page className="file-page" filePage>
|
||||||
|
<div className={classnames('section card-stack', `file-page__${renderMode}`)}>
|
||||||
|
{renderFilePageLayout()}
|
||||||
|
<FileValues uri={uri} />
|
||||||
|
<FileDetails uri={uri} />
|
||||||
|
|
||||||
|
<CommentsList uri={uri} linkedComment={linkedComment} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RecommendedContent uri={uri} />
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilePage;
|
export default FilePage;
|
||||||
|
|
|
@ -51,12 +51,12 @@ const select = (state, props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
uri,
|
||||||
claim: makeSelectClaimForUri(uri)(state),
|
claim: makeSelectClaimForUri(uri)(state),
|
||||||
isResolvingUri: makeSelectIsUriResolving(uri)(state),
|
isResolvingUri: makeSelectIsUriResolving(uri)(state),
|
||||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
totalPages: makeSelectTotalPagesForChannel(uri, PAGE_SIZE)(state),
|
totalPages: makeSelectTotalPagesForChannel(uri, PAGE_SIZE)(state),
|
||||||
isSubscribed: makeSelectChannelInSubscriptions(uri)(state),
|
isSubscribed: makeSelectChannelInSubscriptions(uri)(state),
|
||||||
uri,
|
|
||||||
title: makeSelectTitleForUri(uri)(state),
|
title: makeSelectTitleForUri(uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(uri)(state),
|
claimIsMine: makeSelectClaimIsMine(uri)(state),
|
||||||
claimIsPending: makeSelectClaimIsPending(uri)(state),
|
claimIsPending: makeSelectClaimIsPending(uri)(state),
|
||||||
|
|
|
@ -26,7 +26,6 @@ import {
|
||||||
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
||||||
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
import { selectFloatingUri } from 'redux/selectors/content';
|
|
||||||
|
|
||||||
const DOWNLOAD_POLL_INTERVAL = 1000;
|
const DOWNLOAD_POLL_INTERVAL = 1000;
|
||||||
|
|
||||||
|
@ -128,37 +127,34 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
|
||||||
// @endif
|
// @endif
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doSetPlayingUri(uri: ?string) {
|
export function doSetPrimaryUri(uri: ?string) {
|
||||||
|
return (dispatch: Dispatch) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SET_PRIMARY_URI,
|
||||||
|
data: { uri },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSetPlayingUri({
|
||||||
|
uri,
|
||||||
|
source,
|
||||||
|
pathname,
|
||||||
|
commentId,
|
||||||
|
}: {
|
||||||
|
uri: ?string,
|
||||||
|
source?: string,
|
||||||
|
commentId?: string,
|
||||||
|
pathname: string,
|
||||||
|
}) {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: Dispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SET_PLAYING_URI,
|
type: ACTIONS.SET_PLAYING_URI,
|
||||||
data: { uri },
|
data: { uri, source, pathname, commentId },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doSetFloatingUri(uri: ?string) {
|
|
||||||
return (dispatch: Dispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.SET_FLOATING_URI,
|
|
||||||
data: { uri },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doCloseFloatingPlayer() {
|
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
|
||||||
const state = getState();
|
|
||||||
const floatingUri = selectFloatingUri(state);
|
|
||||||
|
|
||||||
if (floatingUri) {
|
|
||||||
dispatch(doSetFloatingUri(null));
|
|
||||||
} else {
|
|
||||||
dispatch(doSetPlayingUri(null));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolean, cb: ?(GetResponse) => void) {
|
export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolean, cb: ?(GetResponse) => void) {
|
||||||
return (dispatch: Dispatch, getState: () => any) => {
|
return (dispatch: Dispatch, getState: () => any) => {
|
||||||
function onSuccess(fileInfo) {
|
function onSuccess(fileInfo) {
|
||||||
|
|
|
@ -2,21 +2,27 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
|
|
||||||
const reducers = {};
|
const reducers = {};
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
|
primaryUri: null, // Top level content uri triggered from the file page
|
||||||
playingUri: null,
|
playingUri: null,
|
||||||
floatingUri: null,
|
|
||||||
channelClaimCounts: {},
|
channelClaimCounts: {},
|
||||||
positions: {},
|
positions: {},
|
||||||
history: [],
|
history: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.SET_PLAYING_URI] = (state, action) =>
|
reducers[ACTIONS.SET_PRIMARY_URI] = (state, action) =>
|
||||||
Object.assign({}, state, {
|
Object.assign({}, state, {
|
||||||
playingUri: action.data.uri,
|
primaryUri: action.data.uri,
|
||||||
});
|
});
|
||||||
|
|
||||||
reducers[ACTIONS.SET_FLOATING_URI] = (state, action) =>
|
reducers[ACTIONS.SET_PLAYING_URI] = (state, action) =>
|
||||||
Object.assign({}, state, {
|
Object.assign({}, state, {
|
||||||
floatingUri: action.data.uri,
|
playingUri: {
|
||||||
|
uri: action.data.uri,
|
||||||
|
source: action.data.source,
|
||||||
|
pathname: action.data.pathname,
|
||||||
|
commentId: action.data.commentId,
|
||||||
|
primaryUri: state.primaryUri,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
reducers[ACTIONS.SET_CONTENT_POSITION] = (state, action) => {
|
reducers[ACTIONS.SET_CONTENT_POSITION] = (state, action) => {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
makeSelectMediaTypeForUri,
|
makeSelectMediaTypeForUri,
|
||||||
selectBalance,
|
selectBalance,
|
||||||
parseURI,
|
parseURI,
|
||||||
buildURI,
|
|
||||||
makeSelectContentTypeForUri,
|
makeSelectContentTypeForUri,
|
||||||
makeSelectFileNameForUri,
|
makeSelectFileNameForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
|
@ -27,28 +26,23 @@ const HISTORY_ITEMS_PER_PAGE = 50;
|
||||||
export const selectState = (state: any) => state.content || {};
|
export const selectState = (state: any) => state.content || {};
|
||||||
|
|
||||||
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
|
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
|
||||||
export const selectFloatingUri = createSelector(selectState, state => state.floatingUri);
|
export const selectPrimaryUri = createSelector(selectState, state => state.primaryUri);
|
||||||
|
|
||||||
export const makeSelectIsPlaying = (uri: string) => createSelector(selectPlayingUri, playingUri => playingUri === uri);
|
export const makeSelectIsPlaying = (uri: string) => createSelector(selectPrimaryUri, primaryUri => primaryUri === uri);
|
||||||
|
|
||||||
// below is dumb, some context: https://stackoverflow.com/questions/39622864/access-react-router-state-in-selector
|
|
||||||
export const makeSelectIsPlayerFloating = (location: UrlLocation) =>
|
export const makeSelectIsPlayerFloating = (location: UrlLocation) =>
|
||||||
createSelector(selectFloatingUri, selectPlayingUri, selectClaimsByUri, (floatingUri, playingUri, claimsByUri) => {
|
createSelector(selectPrimaryUri, selectPlayingUri, selectClaimsByUri, (primaryUri, playingUri, claimsByUri) => {
|
||||||
if (playingUri && floatingUri && playingUri !== floatingUri) {
|
const isInlineSecondaryPlayer =
|
||||||
return true;
|
playingUri &&
|
||||||
|
playingUri.uri !== primaryUri &&
|
||||||
|
location.pathname === playingUri.pathname &&
|
||||||
|
(playingUri.source === 'comment' || playingUri.source === 'markdown');
|
||||||
|
|
||||||
|
if ((playingUri && playingUri.uri === primaryUri) || isInlineSecondaryPlayer) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no floatingPlayer explicitly set, see if the playingUri can float
|
return true;
|
||||||
try {
|
|
||||||
const { pathname } = location;
|
|
||||||
const { streamName, streamClaimId, channelName, channelClaimId } = parseURI(pathname.slice(1).replace(/:/g, '#'));
|
|
||||||
const pageUrl = buildURI({ streamName, streamClaimId, channelName, channelClaimId });
|
|
||||||
const claimFromUrl = claimsByUri[pageUrl];
|
|
||||||
const playingClaim = claimsByUri[playingUri];
|
|
||||||
return (claimFromUrl && claimFromUrl.claim_id) !== (playingClaim && playingClaim.claim_id);
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
return !!playingUri;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectContentPositionForUri = (uri: string) =>
|
export const makeSelectContentPositionForUri = (uri: string) =>
|
||||||
|
|
|
@ -526,3 +526,7 @@
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.claim-link {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ $thumbnailWidthSmall: 0rem;
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-thumbnail {
|
.comment__author-thumbnail {
|
||||||
@include handleChannelGif($thumbnailWidthSmall);
|
@include handleChannelGif($thumbnailWidthSmall);
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ $thumbnailWidthSmall: 0rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__author-thumbnail {
|
.comment__thumbnail-wrapper {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
.content__viewer--inline {
|
.content__viewer--inline {
|
||||||
max-height: var(--inline-player-max-height);
|
max-height: var(--inline-player-max-height);
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content__viewer--floating {
|
.content__viewer--floating {
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
border-top-right-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-black);
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
.expandable--closed {
|
.expandable--closed {
|
||||||
max-height: 10rem;
|
max-height: 10rem;
|
||||||
overflow: hidden;
|
overflow-y: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
-webkit-mask-image: -webkit-gradient(linear, left 30%, left bottom, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
|
-webkit-mask-image: -webkit-gradient(linear, left 30%, left bottom, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
|
|
||||||
.file-properties--large {
|
.file-properties--large {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: var(--spacing-l);
|
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
|
|
|
@ -50,7 +50,31 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: var(--inline-player-max-height);
|
max-height: var(--inline-player-max-height);
|
||||||
border-radius: var(--card-radius);
|
}
|
||||||
|
|
||||||
|
.file-render--video {
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-color: black;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
animation: fadeInFromBlack 2s ease;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInFromBlack {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-render--embed {
|
.file-render--embed {
|
||||||
|
@ -221,17 +245,20 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
opacity: 1;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
font-size: var(--font-large);
|
font-size: var(--font-large);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background-repeat: repeat-x;
|
border-top-left-radius: var(--border-radius);
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
|
border-top-right-radius: var(--border-radius);
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
.button__label {
|
.button__label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -252,8 +279,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-viewer__embedded-gradient {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: linear-gradient(#000000, #00000000 70%);
|
||||||
|
height: 75px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.file-viewer__embedded-title {
|
.file-viewer__embedded-title {
|
||||||
max-width: 75%;
|
max-width: 75%;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer__embedded-info {
|
.file-viewer__embedded-info {
|
||||||
|
@ -343,7 +382,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vjs-control-bar {
|
.vjs-control-bar {
|
||||||
background-color: #00000095;
|
background-color: #000000;
|
||||||
|
|
||||||
.vjs-remaining-time {
|
.vjs-remaining-time {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -355,6 +394,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vjs-picture-in-picture-control {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-js:not(.vjs-fullscreen).vjs-layout-small {
|
.video-js:not(.vjs-fullscreen).vjs-layout-small {
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image
|
// Image
|
||||||
img {
|
img:not(.channel-thumbnail__custom) {
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
padding-top: var(--spacing-m);
|
padding-top: var(--spacing-m);
|
||||||
max-height: var(--inline-player-max-height);
|
max-height: var(--inline-player-max-height);
|
||||||
|
@ -178,9 +178,40 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-link__url {
|
||||||
|
font-size: var(--font-xxsmall);
|
||||||
|
margin-top: 0;
|
||||||
|
padding-left: var(--spacing-xs);
|
||||||
|
background-color: black;
|
||||||
|
color: var(--color-gray-2);
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
border-bottom-left-radius: var(--border-radius);
|
||||||
|
border-bottom-right-radius: var(--border-radius);
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
height: 2rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--color-gray-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 90px;
|
||||||
|
background: linear-gradient(to top, #000000, #00000000 70%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.preview-link {
|
.preview-link {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: var(--spacing-s) 0;
|
margin: 0;
|
||||||
padding-right: var(--spacing-s);
|
padding-right: var(--spacing-s);
|
||||||
background-color: var(--color-primary-alt);
|
background-color: var(--color-primary-alt);
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -206,12 +237,21 @@
|
||||||
.preview-link__thumbnail {
|
.preview-link__thumbnail {
|
||||||
width: 12rem;
|
width: 12rem;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-link__thumbnail--channel {
|
||||||
|
width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-link__description {
|
.preview-link__description {
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-viewer__embedded-header {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-preview {
|
.editor-preview {
|
||||||
|
|
|
@ -298,7 +298,7 @@ textarea {
|
||||||
.thumbnail-preview {
|
.thumbnail-preview {
|
||||||
width: var(--thumbnail-preview-width);
|
width: var(--thumbnail-preview-width);
|
||||||
height: var(--thumbnail-preview-height);
|
height: var(--thumbnail-preview-height);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
|
|
@ -38,6 +38,7 @@ $breakpoint-large: 1600px;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
--font-base: 14px;
|
--font-base: 14px;
|
||||||
--font-body: 1rem;
|
--font-body: 1rem;
|
||||||
|
--font-xxsmall: 0.65rem;
|
||||||
--font-xsmall: 0.7344rem;
|
--font-xsmall: 0.7344rem;
|
||||||
--font-small: 0.8571rem;
|
--font-small: 0.8571rem;
|
||||||
--font-large: 1.3rem;
|
--font-large: 1.3rem;
|
||||||
|
@ -67,7 +68,9 @@ $breakpoint-large: 1600px;
|
||||||
--tab-indicator-size: 0.5rem;
|
--tab-indicator-size: 0.5rem;
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
--header-height: 5rem;
|
// This is tied to the floating player so it knows where to attach to
|
||||||
|
// ui/component/fileRenderFloating/view.jsx
|
||||||
|
--header-height: 80px;
|
||||||
|
|
||||||
// Inline Player
|
// Inline Player
|
||||||
--inline-player-max-height: calc(100vh - var(--header-height) - var(--spacing-l) * 2);
|
--inline-player-max-height: calc(100vh - var(--header-height) - var(--spacing-l) * 2);
|
||||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -7391,13 +7391,13 @@ lazy-val@^1.0.4:
|
||||||
yargs "^13.2.2"
|
yargs "^13.2.2"
|
||||||
zstd-codec "^0.1.1"
|
zstd-codec "^0.1.1"
|
||||||
|
|
||||||
lbry-redux@lbryio/lbry-redux#15737f9b098f3654122a75f93d63584e7ffa17a1:
|
lbry-redux@lbryio/lbry-redux#823197af37da745fd79632b9e82deddf9d7fe033:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/15737f9b098f3654122a75f93d63584e7ffa17a1"
|
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/823197af37da745fd79632b9e82deddf9d7fe033"
|
||||||
dependencies:
|
dependencies:
|
||||||
proxy-polyfill "0.1.6"
|
proxy-polyfill "0.1.6"
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
uuid "^3.3.2"
|
uuid "^8.3.1"
|
||||||
|
|
||||||
lbryinc@lbryio/lbryinc#db0663fcc4a64cb082b6edc5798fafa67eb4300f:
|
lbryinc@lbryio/lbryinc#db0663fcc4a64cb082b6edc5798fafa67eb4300f:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
|
@ -12130,6 +12130,11 @@ uuid@^8.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea"
|
||||||
integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==
|
integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==
|
||||||
|
|
||||||
|
uuid@^8.3.1:
|
||||||
|
version "8.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
|
||||||
|
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
|
||||||
|
|
||||||
v8-compile-cache@2.0.3:
|
v8-compile-cache@2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
|
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
|
||||||
|
|
Loading…
Reference in a new issue