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
|
||||
# preventing user settings from applying to custom sites without overwriting them.
|
||||
# UNSYNCED_SETTINGS='theme dark_mode_times automatic_dark_mode_enabled'
|
||||
KNOWN_APP_DOMAINS=lbry.tv,lbry.lat,odysee.com
|
||||
|
||||
# Custom Content
|
||||
# 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_URI_2: process.env.PINNED_URI_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}`;
|
||||
|
|
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",
|
||||
"json-loader": "^0.5.4",
|
||||
"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",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
|
|
@ -1329,7 +1329,7 @@
|
|||
"Replying as": "Replying as",
|
||||
"No uploads": "No uploads",
|
||||
"uploads": "uploads",
|
||||
"Discussion": "Discussion",
|
||||
"Community": "Community",
|
||||
"Staked LBRY Credits": "Staked LBRY Credits",
|
||||
"1 comment": "1 comment",
|
||||
"%total_comments% comments": "%total_comments% comments",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doResolveUri, makeSelectTitleForUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux';
|
||||
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||
import { selectPlayingUri } from 'redux/selectors/content';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import ClaimLink from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -10,11 +12,11 @@ const select = (state, props) => {
|
|||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ClaimLink);
|
||||
export default connect(select, {
|
||||
doResolveUri,
|
||||
doSetPlayingUri,
|
||||
})(ClaimLink);
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import PreviewLink from 'component/previewLink';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import classnames from 'classnames';
|
||||
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 = {
|
||||
uri: string,
|
||||
claim: StreamClaim,
|
||||
children: React.Node,
|
||||
autoEmbed: ?boolean,
|
||||
description: ?string,
|
||||
isResolvingUri: boolean,
|
||||
resolveUri: string => void,
|
||||
doResolveUri: string => void,
|
||||
blackListedOutpoints: Array<{
|
||||
txid: string,
|
||||
nout: number,
|
||||
}>,
|
||||
playingUri: ?PlayingUri,
|
||||
parentCommentId?: string,
|
||||
isMarkdownPost?: boolean,
|
||||
};
|
||||
|
||||
class ClaimLink extends React.Component<Props> {
|
||||
|
@ -22,7 +27,6 @@ class ClaimLink extends React.Component<Props> {
|
|||
href: null,
|
||||
link: false,
|
||||
thumbnail: null,
|
||||
autoEmbed: false,
|
||||
description: null,
|
||||
isResolvingUri: false,
|
||||
};
|
||||
|
@ -51,17 +55,22 @@ class ClaimLink extends React.Component<Props> {
|
|||
}
|
||||
|
||||
resolve = (props: Props) => {
|
||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||
const { isResolvingUri, doResolveUri, claim, uri } = props;
|
||||
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
resolveUri(uri);
|
||||
doResolveUri(uri);
|
||||
}
|
||||
};
|
||||
|
||||
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 isBlacklisted = this.isClaimBlackListed();
|
||||
const isPlayingInline =
|
||||
playingUri &&
|
||||
playingUri.uri === uri &&
|
||||
((playingUri.source === 'comment' && parentCommentId === playingUri.commentId) ||
|
||||
playingUri.source === 'markdown');
|
||||
|
||||
if (isBlacklisted || isUnresolved) {
|
||||
return <span>{children}</span>;
|
||||
|
@ -69,13 +78,23 @@ class ClaimLink extends React.Component<Props> {
|
|||
|
||||
const { value_type: valueType } = claim;
|
||||
const isChannel = valueType === 'channel';
|
||||
const showPreview = autoEmbed === true && !isUnresolved;
|
||||
|
||||
if (isChannel) {
|
||||
return <UriIndicator uri={uri} link addTooltip />;
|
||||
}
|
||||
|
||||
return <React.Fragment>{showPreview && <PreviewLink uri={uri} />}</React.Fragment>;
|
||||
return isChannel ? (
|
||||
<div className="card--inline">
|
||||
<ClaimPreview uri={uri} wrapperElement="div" />
|
||||
</div>
|
||||
) : (
|
||||
<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,
|
||||
hideActions?: boolean,
|
||||
renderActions?: Claim => ?Node,
|
||||
wrapperElement?: string,
|
||||
};
|
||||
|
||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||
|
@ -95,7 +96,9 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
includeSupportAction,
|
||||
hideActions = false,
|
||||
renderActions,
|
||||
wrapperElement,
|
||||
} = props;
|
||||
const WrapperElement = wrapperElement || 'li';
|
||||
const shouldFetch =
|
||||
claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending);
|
||||
const abandoned = !isResolvingUri && !claim;
|
||||
|
@ -221,7 +224,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
<WrapperElement
|
||||
ref={ref}
|
||||
role="link"
|
||||
onClick={pending || type === 'inline' ? undefined : handleOnClick}
|
||||
|
@ -321,7 +324,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</WrapperElement>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -188,11 +188,11 @@ function Comment(props: Props) {
|
|||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||
})}
|
||||
>
|
||||
<div className="comment__author-thumbnail">
|
||||
<div className="comment__thumbnail-wrapper">
|
||||
{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>
|
||||
|
||||
|
@ -298,10 +298,10 @@ function Comment(props: Props) {
|
|||
</div>
|
||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||
<Expandable>
|
||||
<MarkdownPreview content={message} promptLinks />
|
||||
<MarkdownPreview content={message} promptLinks parentCommentId={commentId} />
|
||||
</Expandable>
|
||||
) : (
|
||||
<MarkdownPreview content={message} promptLinks />
|
||||
<MarkdownPreview content={message} promptLinks parentCommentId={commentId} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ import {
|
|||
selectCommentChannel,
|
||||
} from 'redux/selectors/comments';
|
||||
import { doCommentList, doCommentReactList } from 'redux/actions/comments';
|
||||
import CommentsList from './view';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import CommentsList from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
|
|
|
@ -53,7 +53,9 @@ function CommentList(props: Props) {
|
|||
const [end, setEnd] = React.useState(9);
|
||||
// Display comments immediately if not fetching reactions
|
||||
// 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 hasNoComments = totalComments === 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>}
|
||||
|
||||
<ul className="comments" ref={commentRef}>
|
||||
{!isFetchingComments &&
|
||||
comments &&
|
||||
{comments &&
|
||||
displayedComments &&
|
||||
displayedComments.map(comment => {
|
||||
return (
|
||||
|
|
|
@ -19,8 +19,13 @@ class LoadingScreen extends React.PureComponent<Props> {
|
|||
const { status, spinner, isDocument } = this.props;
|
||||
return (
|
||||
<div className={classnames('content__loading', { 'content__loading--document': isDocument })}>
|
||||
{spinner && <Spinner light={!isDocument} />}
|
||||
{status && <span className={classnames('content__loading-text')}>{status}</span>}
|
||||
{spinner && (
|
||||
<Spinner
|
||||
light={!isDocument}
|
||||
delayed
|
||||
text={status && <span className={classnames('content__loading-text')}>{status}</span>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,12 +7,9 @@ import remarkStrip from 'strip-markdown';
|
|||
import remarkEmoji from 'remark-emoji';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
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 { formatedLinks, inlineLinks } from 'util/remark-lbry';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import EmbedPlayButton from 'component/embedPlayButton';
|
||||
|
||||
type SimpleTextProps = {
|
||||
children?: React.Node,
|
||||
|
@ -22,7 +19,6 @@ type SimpleLinkProps = {
|
|||
href?: string,
|
||||
title?: string,
|
||||
children?: React.Node,
|
||||
noDataStore?: boolean,
|
||||
};
|
||||
|
||||
type MarkdownProps = {
|
||||
|
@ -31,6 +27,8 @@ type MarkdownProps = {
|
|||
promptLinks?: boolean,
|
||||
noDataStore?: boolean,
|
||||
className?: string,
|
||||
parentCommentId?: string,
|
||||
isMarkdownPost?: boolean,
|
||||
};
|
||||
|
||||
const SimpleText = (props: SimpleTextProps) => {
|
||||
|
@ -38,14 +36,13 @@ const SimpleText = (props: SimpleTextProps) => {
|
|||
};
|
||||
|
||||
const SimpleLink = (props: SimpleLinkProps) => {
|
||||
const { title, children } = props;
|
||||
const { href, noDataStore } = props;
|
||||
const { title, children, href } = props;
|
||||
|
||||
if (!href) {
|
||||
return children || null;
|
||||
}
|
||||
|
||||
if (!href.startsWith('lbry://')) {
|
||||
if (!href.startsWith('lbry:/')) {
|
||||
return (
|
||||
<a href={href} title={title} target={'_blank'} rel={'noreferrer noopener'}>
|
||||
{children}
|
||||
|
@ -60,33 +57,15 @@ const SimpleLink = (props: SimpleLinkProps) => {
|
|||
if (embed) {
|
||||
// Decode this since users might just copy it from the url bar
|
||||
const decodedUri = decodeURI(uri);
|
||||
return noDataStore ? (
|
||||
return (
|
||||
<div className="embed__inline-button-preview">
|
||||
<pre>{decodedUri}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<EmbedPlayButton uri={decodedUri} />
|
||||
);
|
||||
}
|
||||
|
||||
const webLink = formatLbryUrlForWeb(uri);
|
||||
// using Link after formatLbryUrl to handle "/" vs "#/"
|
||||
// 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>
|
||||
);
|
||||
// Dummy link (no 'href')
|
||||
return <a title={title}>{children}</a>;
|
||||
};
|
||||
|
||||
// Use github sanitation schema
|
||||
|
@ -99,7 +78,7 @@ schema.attributes.a.push('embed');
|
|||
const REPLACE_REGEX = /(<iframe\s+src=["'])(.*?(?=))(["']\s*><\/iframe>)/g;
|
||||
|
||||
const MarkdownPreview = (props: MarkdownProps) => {
|
||||
const { content, strip, promptLinks, noDataStore, className } = props;
|
||||
const { content, strip, promptLinks, noDataStore, className, parentCommentId, isMarkdownPost } = props;
|
||||
const strippedContent = content
|
||||
? content.replace(REPLACE_REGEX, (iframeHtml, y, iframeSrc) => {
|
||||
// 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;
|
||||
|
||||
if (src && src.startsWith('lbry://')) {
|
||||
return `${src}?embed=true`;
|
||||
return src;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,7 +102,16 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
|||
sanitize: schema,
|
||||
fragment: React.Fragment,
|
||||
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
|
||||
div: React.Fragment,
|
||||
},
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri, SETTINGS } from 'lbry-redux';
|
||||
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 { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
|
||||
import ChannelThumbnail from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
|
@ -11,12 +13,13 @@ const select = (state, props) => ({
|
|||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doResolveUri,
|
||||
doFetchCostInfoForUri,
|
||||
doSetFloatingUri,
|
||||
doPlayUri,
|
||||
doSetPlayingUri,
|
||||
doAnaltyicsPurchaseEvent,
|
||||
})(ChannelThumbnail);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// @flow
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import React, { useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Button from 'component/button';
|
||||
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
@ -12,11 +14,14 @@ type Props = {
|
|||
claim: ?Claim,
|
||||
doResolveUri: string => void,
|
||||
doFetchCostInfoForUri: string => void,
|
||||
doSetFloatingUri: string => void,
|
||||
costInfo: ?{ cost: number },
|
||||
floatingPlayerEnabled: boolean,
|
||||
doPlayUri: (string, ?boolean, ?boolean, (GetResponse) => void) => void,
|
||||
doAnaltyicsPurchaseEvent: GetResponse => void,
|
||||
parentCommentId?: string,
|
||||
isMarkdownPost: boolean,
|
||||
doSetPlayingUri: ({}) => void,
|
||||
renderMode: string,
|
||||
};
|
||||
|
||||
export default function EmbedPlayButton(props: Props) {
|
||||
|
@ -26,33 +31,54 @@ export default function EmbedPlayButton(props: Props) {
|
|||
claim,
|
||||
doResolveUri,
|
||||
doFetchCostInfoForUri,
|
||||
doSetFloatingUri,
|
||||
floatingPlayerEnabled,
|
||||
doPlayUri,
|
||||
doSetPlayingUri,
|
||||
doAnaltyicsPurchaseEvent,
|
||||
costInfo,
|
||||
parentCommentId,
|
||||
isMarkdownPost,
|
||||
renderMode,
|
||||
} = props;
|
||||
const { push } = useHistory();
|
||||
const {
|
||||
push,
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
const isMobile = useIsMobile();
|
||||
const hasResolvedUri = claim !== undefined;
|
||||
const hasCostInfo = costInfo !== undefined;
|
||||
const disabled = !hasResolvedUri || !costInfo;
|
||||
const canPlayInline = [RENDER_MODES.AUDIO, RENDER_MODES.VIDEO].includes(renderMode);
|
||||
|
||||
useEffect(() => {
|
||||
doResolveUri(uri);
|
||||
doFetchCostInfoForUri(uri);
|
||||
}, [uri, doResolveUri, doFetchCostInfoForUri]);
|
||||
if (!hasResolvedUri) {
|
||||
doResolveUri(uri);
|
||||
}
|
||||
|
||||
if (!hasCostInfo) {
|
||||
doFetchCostInfoForUri(uri);
|
||||
}
|
||||
}, [uri, doResolveUri, doFetchCostInfoForUri, hasCostInfo, hasResolvedUri]);
|
||||
|
||||
function handleClick() {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMobile || !floatingPlayerEnabled) {
|
||||
if (isMobile || !floatingPlayerEnabled || !canPlayInline) {
|
||||
const formattedUrl = formatLbryUrlForWeb(uri);
|
||||
push(formattedUrl);
|
||||
} else {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
@ -67,7 +93,16 @@ export default function EmbedPlayButton(props: Props) {
|
|||
style={{ backgroundImage: `url('${thumbnail.replace(/'/g, "\\'")}')` }}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
'file-render--document': RENDER_MODES.TEXT_MODES.includes(renderMode) && !embedded,
|
||||
'file-render--embed': embedded,
|
||||
'file-render--video': renderMode === RENDER_MODES.VIDEO || renderMode === RENDER_MODES.AUDIO,
|
||||
})}
|
||||
>
|
||||
{this.renderViewer()}
|
||||
|
|
|
@ -2,21 +2,24 @@ import { connect } from 'react-redux';
|
|||
import { makeSelectFileInfoForUri, makeSelectTitleForUri, makeSelectStreamingUrlForUri, SETTINGS } from 'lbry-redux';
|
||||
import {
|
||||
makeSelectIsPlayerFloating,
|
||||
selectFloatingUri,
|
||||
selectPrimaryUri,
|
||||
selectPlayingUri,
|
||||
makeSelectFileRenderModeForUri,
|
||||
} from 'redux/selectors/content';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doCloseFloatingPlayer } from 'redux/actions/content';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import { withRouter } from 'react-router';
|
||||
import FileRenderFloating from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const floatingUri = selectFloatingUri(state);
|
||||
const playingUri = selectPlayingUri(state);
|
||||
const uri = floatingUri || playingUri;
|
||||
const primaryUri = selectPrimaryUri(state);
|
||||
const uri = playingUri && playingUri.uri;
|
||||
|
||||
return {
|
||||
uri,
|
||||
primaryUri,
|
||||
playingUri,
|
||||
title: makeSelectTitleForUri(uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(uri)(state),
|
||||
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
||||
|
@ -27,7 +30,7 @@ const select = (state, props) => {
|
|||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
closeFloatingPlayer: () => dispatch(doCloseFloatingPlayer(null)),
|
||||
closeFloatingPlayer: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(FileRenderFloating));
|
||||
|
|
|
@ -8,14 +8,16 @@ import LoadingScreen from 'component/common/loading-screen';
|
|||
import FileRender from 'component/fileRender';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
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 Tooltip from 'component/common/tooltip';
|
||||
import { onFullscreenChange } from 'util/full-screen';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import debounce from 'util/debounce';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
|
||||
export const INLINE_PLAYER_WRAPPER_CLASS = 'inline-player__wrapper';
|
||||
|
||||
type Props = {
|
||||
isFloating: boolean,
|
||||
|
@ -26,6 +28,8 @@ type Props = {
|
|||
floatingPlayerEnabled: boolean,
|
||||
closeFloatingPlayer: () => void,
|
||||
renderMode: string,
|
||||
playingUri: ?PlayingUri,
|
||||
primaryUri: ?string,
|
||||
};
|
||||
|
||||
export default function FileRenderFloating(props: Props) {
|
||||
|
@ -38,9 +42,14 @@ export default function FileRenderFloating(props: Props) {
|
|||
closeFloatingPlayer,
|
||||
floatingPlayerEnabled,
|
||||
renderMode,
|
||||
playingUri,
|
||||
primaryUri,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
const isMobile = useIsMobile();
|
||||
const mainFilePlaying = playingUri && playingUri.uri === primaryUri;
|
||||
const [fileViewerRect, setFileViewerRect] = useState();
|
||||
const [desktopPlayStartTime, setDesktopPlayStartTime] = useState();
|
||||
const [wasDragging, setWasDragging] = useState(false);
|
||||
|
@ -48,8 +57,12 @@ export default function FileRenderFloating(props: Props) {
|
|||
x: -25,
|
||||
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 isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
|
||||
const loadingMessage =
|
||||
|
@ -98,15 +111,18 @@ export default function FileRenderFloating(props: Props) {
|
|||
}, []);
|
||||
|
||||
// Ensure player is within screen when 'isFloating' changes.
|
||||
const stringifiedPosition = JSON.stringify(position);
|
||||
useEffect(() => {
|
||||
const jsonPosition = JSON.parse(stringifiedPosition);
|
||||
|
||||
if (isFloating) {
|
||||
let pos = { x: position.x, y: position.y };
|
||||
let pos = { x: jsonPosition.x, y: jsonPosition.y };
|
||||
clampToScreen(pos);
|
||||
if (pos.x !== position.x || pos.y !== position.y) {
|
||||
setPosition({ x: pos.x, y: pos.y });
|
||||
}
|
||||
}
|
||||
}, [isFloating]);
|
||||
}, [isFloating, stringifiedPosition]);
|
||||
|
||||
// Listen to main-window resizing and adjust the fp position accordingly:
|
||||
useEffect(() => {
|
||||
|
@ -126,27 +142,37 @@ export default function FileRenderFloating(props: Props) {
|
|||
// Otherwise, this could just be changed to a one-time effect.
|
||||
}, [relativePos]);
|
||||
|
||||
// Update 'fileViewerRect':
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
function handleResize() {
|
||||
const element = mainFilePlaying
|
||||
? document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`)
|
||||
: document.querySelector(`.${INLINE_PLAYER_WRAPPER_CLASS}`);
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
// $FlowFixMe
|
||||
setFileViewerRect(rect);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
// $FlowFixMe
|
||||
setFileViewerRect(rect);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (streamingUrl) {
|
||||
handleResize();
|
||||
}
|
||||
}, [streamingUrl, pathname, playingUriSource, isFloating, mainFilePlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
onFullscreenChange(window, 'add', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
onFullscreenChange(window, 'remove', handleResize);
|
||||
};
|
||||
}, [setFileViewerRect, isFloating]);
|
||||
}, [setFileViewerRect, isFloating, playingUriSource, mainFilePlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
// @if TARGET='app'
|
||||
|
@ -210,7 +236,13 @@ export default function FileRenderFloating(props: Props) {
|
|||
})}
|
||||
style={
|
||||
!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 { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||
import { doPlayUri, doSetPlayingUri, doSetPrimaryUri } from 'redux/actions/content';
|
||||
import {
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
|
@ -41,7 +41,8 @@ const select = (state, props) => ({
|
|||
|
||||
const perform = dispatch => ({
|
||||
play: uri => {
|
||||
dispatch(doSetPlayingUri(uri));
|
||||
dispatch(doSetPrimaryUri(uri));
|
||||
dispatch(doSetPlayingUri({ uri }));
|
||||
dispatch(doPlayUri(uri, undefined, undefined, fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -52,7 +52,6 @@ export default function FileRenderInitiator(props: Props) {
|
|||
claimWasPurchased,
|
||||
authenticated,
|
||||
} = props;
|
||||
|
||||
const cost = costInfo && costInfo.cost;
|
||||
const isFree = hasCostInfo && cost === 0;
|
||||
const fileStatus = fileInfo && fileInfo.status;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'lbry-redux';
|
||||
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 { doAnalyticsView } from 'redux/actions/app';
|
||||
import FileRenderInline from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||
isPlaying: selectPrimaryUri(state) === props.uri,
|
||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ function FileViewerEmbeddedTitle(props: Props) {
|
|||
|
||||
return (
|
||||
<div className="file-viewer__embedded-header">
|
||||
<div className="file-viewer__embedded-gradient" />
|
||||
<Button label={title} button="link" className="file-viewer__embedded-title" {...contentLinkProps} />
|
||||
<div className="file-viewer__embedded-info">
|
||||
<Button className="file-viewer__overlay-logo" icon={ICONS.LBRY} {...lbryLinkProps} />
|
||||
|
|
|
@ -7,7 +7,4 @@ const perform = dispatch => ({
|
|||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ExternalLink);
|
||||
export default connect(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 { withRouter } from 'react-router-dom';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -22,6 +24,7 @@ class PreviewLink extends React.PureComponent<Props> {
|
|||
|
||||
render() {
|
||||
const { uri, title, description, thumbnail } = this.props;
|
||||
const { isChannel } = parseURI(uri);
|
||||
const placeholder = 'static/img/placeholder.png';
|
||||
|
||||
const thumbnailStyle = {
|
||||
|
@ -31,7 +34,12 @@ class PreviewLink extends React.PureComponent<Props> {
|
|||
return (
|
||||
<span className="preview-link" role="button" onClick={this.handleClick}>
|
||||
<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-info">
|
||||
<span className="claim-preview__title">
|
||||
|
|
|
@ -9,6 +9,7 @@ type Props = {
|
|||
theme: string,
|
||||
type: ?string,
|
||||
delayed: boolean,
|
||||
text?: any,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -54,7 +55,7 @@ class Spinner extends PureComponent<Props, State> {
|
|||
delayedTimeout: ?TimeoutID;
|
||||
|
||||
render() {
|
||||
const { dark, light, theme, type } = this.props;
|
||||
const { dark, light, theme, type, text } = this.props;
|
||||
const { show } = this.state;
|
||||
|
||||
if (!show) {
|
||||
|
@ -62,19 +63,22 @@ class Spinner extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('spinner', {
|
||||
'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 rect4" />
|
||||
<div className="rect rect5" />
|
||||
</div>
|
||||
<>
|
||||
{text}
|
||||
<div
|
||||
className={classnames('spinner', {
|
||||
'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 rect4" />
|
||||
<div className="rect rect5" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
|
|||
const { contentType } = source;
|
||||
|
||||
return renderMode === RENDER_MODES.MARKDOWN ? (
|
||||
<Card body={<MarkdownPreview content={content} />} />
|
||||
<Card body={<MarkdownPreview content={content} isMarkdownPost promptLinks />} />
|
||||
) : (
|
||||
<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_COMPLETED = 'PUBLISH_COMPLETED';
|
||||
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_FLOATING_URI = 'SET_FLOATING_URI';
|
||||
export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION';
|
||||
export const CLEAR_CONTENT_POSITION = 'CLEAR_CONTENT_POSITION';
|
||||
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { doHideModal, doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
||||
import { makeSelectMetadataForUri } from 'lbry-redux';
|
||||
|
@ -12,8 +12,7 @@ const select = (state, props) => ({
|
|||
|
||||
const perform = dispatch => ({
|
||||
analyticsPurchaseEvent: fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo)),
|
||||
setPlayingUri: uri => dispatch(doSetPlayingUri(uri)),
|
||||
setFloatingUri: uri => dispatch(doSetFloatingUri(uri)),
|
||||
setPlayingUri: uri => dispatch(doSetPlayingUri({ uri })),
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
loadVideo: (uri, onSuccess) => dispatch(doPlayUri(uri, true, undefined, onSuccess)),
|
||||
});
|
||||
|
|
|
@ -17,9 +17,8 @@ type Props = {
|
|||
cancelPurchase: () => void,
|
||||
metadata: StreamMetadata,
|
||||
analyticsPurchaseEvent: GetResponse => void,
|
||||
playingUri: ?string,
|
||||
playingUri: ?PlayingUri,
|
||||
setPlayingUri: (?string) => void,
|
||||
setFloatingUri: (?string) => void,
|
||||
};
|
||||
|
||||
function ModalAffirmPurchase(props: Props) {
|
||||
|
@ -31,11 +30,9 @@ function ModalAffirmPurchase(props: Props) {
|
|||
analyticsPurchaseEvent,
|
||||
playingUri,
|
||||
setPlayingUri,
|
||||
setFloatingUri,
|
||||
} = props;
|
||||
const [success, setSuccess] = React.useState(false);
|
||||
const [purchasing, setPurchasing] = React.useState(false);
|
||||
|
||||
const modalTitle = __('Confirm Purchase');
|
||||
|
||||
function onAffirmPurchase() {
|
||||
|
@ -45,14 +42,14 @@ function ModalAffirmPurchase(props: Props) {
|
|||
setSuccess(true);
|
||||
analyticsPurchaseEvent(fileInfo);
|
||||
|
||||
if (playingUri !== uri) {
|
||||
setFloatingUri(uri);
|
||||
if (!playingUri || playingUri.uri !== uri) {
|
||||
setPlayingUri(uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancelPurchase() {
|
||||
if (uri === playingUri) {
|
||||
if (playingUri && uri === playingUri.uri) {
|
||||
setPlayingUri(null);
|
||||
}
|
||||
|
||||
|
|
|
@ -201,7 +201,7 @@ function ChannelPage(props: Props) {
|
|||
<TabList className="tabs__list--channel-page">
|
||||
<Tab disabled={editing}>{__('Content')}</Tab>
|
||||
<Tab>{editing ? __('Editing Your Channel') : __('About --[tab title in Channel Page]--')}</Tab>
|
||||
<Tab disabled={editing}>{__('Discussion')}</Tab>
|
||||
<Tab disabled={editing}>{__('Community')}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
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 {
|
||||
doFetchFileInfo,
|
||||
|
@ -39,6 +39,7 @@ const perform = dispatch => ({
|
|||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
|
||||
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
||||
setPrimaryUri: uri => dispatch(doSetPrimaryUri(uri)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(FilePage));
|
||||
|
|
|
@ -12,7 +12,7 @@ import FileValues from 'component/fileValues';
|
|||
import RecommendedContent from 'component/recommendedContent';
|
||||
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 = {
|
||||
costInfo: ?{ includesData: boolean, cost: number },
|
||||
|
@ -28,63 +28,61 @@ type Props = {
|
|||
obscureNsfw: boolean,
|
||||
isMature: boolean,
|
||||
linkedComment: any,
|
||||
setPrimaryUri: (?string) => void,
|
||||
};
|
||||
|
||||
class FilePage extends React.Component<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
this.lastReset = undefined;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { uri, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed } = this.props;
|
||||
|
||||
if (isSubscribed) {
|
||||
this.removeFromSubscriptionNotifications();
|
||||
}
|
||||
function FilePage(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
channelUri,
|
||||
renderMode,
|
||||
fetchFileInfo,
|
||||
fetchCostInfo,
|
||||
setViewed,
|
||||
isSubscribed,
|
||||
fileInfo,
|
||||
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
|
||||
// this could probably be refactored into more direct components now
|
||||
// @if TARGET='app'
|
||||
fetchFileInfo(uri);
|
||||
if (!hasFileInfo) {
|
||||
fetchFileInfo(uri);
|
||||
}
|
||||
// @endif
|
||||
|
||||
// See https://github.com/lbryio/lbry-desktop/pull/1563 for discussion
|
||||
fetchCostInfo(uri);
|
||||
setViewed(uri);
|
||||
}
|
||||
setPrimaryUri(uri);
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { isSubscribed, uri, fileInfo, setViewed, fetchFileInfo } = this.props;
|
||||
return () => {
|
||||
setPrimaryUri(null);
|
||||
};
|
||||
}, [uri, hasFileInfo, fetchFileInfo, fetchCostInfo, setViewed, setPrimaryUri]);
|
||||
|
||||
if (!prevProps.isSubscribed && isSubscribed) {
|
||||
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() {
|
||||
React.useEffect(() => {
|
||||
// Always try to remove
|
||||
// If it doesn't exist, nothing will happen
|
||||
const { markSubscriptionRead, uri, channelUri } = this.props;
|
||||
markSubscriptionRead(channelUri, uri);
|
||||
}
|
||||
if (isSubscribed) {
|
||||
markSubscriptionRead(channelUri, uri);
|
||||
}
|
||||
}, [isSubscribed, markSubscriptionRead, uri, channelUri]);
|
||||
|
||||
renderFilePageLayout(uri: string, mode: string, cost: ?number) {
|
||||
if (RENDER_MODES.FLOATING_MODES.includes(mode)) {
|
||||
function renderFilePageLayout() {
|
||||
if (RENDER_MODES.FLOATING_MODES.includes(renderMode)) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={FILE_WRAPPER_CLASS}>
|
||||
<div className={PRIMARY_PLAYER_WRAPPER_CLASS}>
|
||||
<FileRenderInitiator uri={uri} />
|
||||
</div>
|
||||
{/* 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 (
|
||||
<React.Fragment>
|
||||
<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 (
|
||||
<React.Fragment>
|
||||
<FileTitle uri={uri} />
|
||||
|
@ -121,8 +119,7 @@ class FilePage extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
renderBlockedPage() {
|
||||
const { uri } = this.props;
|
||||
function renderBlockedPage() {
|
||||
return (
|
||||
<Page>
|
||||
<FileTitle uri={uri} isNsfwBlocked />
|
||||
|
@ -130,29 +127,23 @@ class FilePage extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
lastReset: ?any;
|
||||
|
||||
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>
|
||||
);
|
||||
if (obscureNsfw && isMature) {
|
||||
return renderBlockedPage();
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -51,12 +51,12 @@ const select = (state, props) => {
|
|||
}
|
||||
|
||||
return {
|
||||
uri,
|
||||
claim: makeSelectClaimForUri(uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(uri)(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||
totalPages: makeSelectTotalPagesForChannel(uri, PAGE_SIZE)(state),
|
||||
isSubscribed: makeSelectChannelInSubscriptions(uri)(state),
|
||||
uri,
|
||||
title: makeSelectTitleForUri(uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(uri)(state),
|
||||
claimIsPending: makeSelectClaimIsPending(uri)(state),
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
||||
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { selectFloatingUri } from 'redux/selectors/content';
|
||||
|
||||
const DOWNLOAD_POLL_INTERVAL = 1000;
|
||||
|
||||
|
@ -128,37 +127,34 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
|
|||
// @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) => {
|
||||
dispatch({
|
||||
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) {
|
||||
return (dispatch: Dispatch, getState: () => any) => {
|
||||
function onSuccess(fileInfo) {
|
||||
|
|
|
@ -2,21 +2,27 @@ import * as ACTIONS from 'constants/action_types';
|
|||
|
||||
const reducers = {};
|
||||
const defaultState = {
|
||||
primaryUri: null, // Top level content uri triggered from the file page
|
||||
playingUri: null,
|
||||
floatingUri: null,
|
||||
channelClaimCounts: {},
|
||||
positions: {},
|
||||
history: [],
|
||||
};
|
||||
|
||||
reducers[ACTIONS.SET_PLAYING_URI] = (state, action) =>
|
||||
reducers[ACTIONS.SET_PRIMARY_URI] = (state, action) =>
|
||||
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, {
|
||||
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) => {
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
makeSelectMediaTypeForUri,
|
||||
selectBalance,
|
||||
parseURI,
|
||||
buildURI,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectFileNameForUri,
|
||||
} from 'lbry-redux';
|
||||
|
@ -27,28 +26,23 @@ const HISTORY_ITEMS_PER_PAGE = 50;
|
|||
export const selectState = (state: any) => state.content || {};
|
||||
|
||||
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) =>
|
||||
createSelector(selectFloatingUri, selectPlayingUri, selectClaimsByUri, (floatingUri, playingUri, claimsByUri) => {
|
||||
if (playingUri && floatingUri && playingUri !== floatingUri) {
|
||||
return true;
|
||||
createSelector(selectPrimaryUri, selectPlayingUri, selectClaimsByUri, (primaryUri, playingUri, claimsByUri) => {
|
||||
const isInlineSecondaryPlayer =
|
||||
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
|
||||
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;
|
||||
return true;
|
||||
});
|
||||
|
||||
export const makeSelectContentPositionForUri = (uri: string) =>
|
||||
|
|
|
@ -526,3 +526,7 @@
|
|||
border-radius: var(--border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
.claim-link {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ $thumbnailWidthSmall: 0rem;
|
|||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.channel-thumbnail {
|
||||
.comment__author-thumbnail {
|
||||
@include handleChannelGif($thumbnailWidthSmall);
|
||||
margin-right: 0;
|
||||
|
||||
|
@ -53,7 +53,7 @@ $thumbnailWidthSmall: 0rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__author-thumbnail {
|
||||
.comment__thumbnail-wrapper {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
.content__viewer--inline {
|
||||
max-height: var(--inline-player-max-height);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.content__viewer--floating {
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
display: flex;
|
||||
justify-content: 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) {
|
||||
height: 200px;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
.expandable--closed {
|
||||
max-height: 10rem;
|
||||
overflow: hidden;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
-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 {
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--spacing-l);
|
||||
margin-left: 0;
|
||||
|
||||
& > * {
|
||||
|
|
|
@ -50,7 +50,31 @@
|
|||
z-index: 1;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
|
@ -221,17 +245,20 @@
|
|||
justify-content: space-between;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
z-index: 2;
|
||||
font-size: var(--font-large);
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
background-repeat: repeat-x;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
|
||||
.button {
|
||||
padding: var(--spacing-s);
|
||||
color: var(--color-white);
|
||||
z-index: 2;
|
||||
|
||||
.button__label {
|
||||
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 {
|
||||
max-width: 75%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.file-viewer__embedded-info {
|
||||
|
@ -343,7 +382,7 @@
|
|||
}
|
||||
|
||||
.vjs-control-bar {
|
||||
background-color: #00000095;
|
||||
background-color: #000000;
|
||||
|
||||
.vjs-remaining-time {
|
||||
display: none;
|
||||
|
@ -355,6 +394,10 @@
|
|||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-picture-in-picture-control {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.video-js:not(.vjs-fullscreen).vjs-layout-small {
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
}
|
||||
|
||||
// Image
|
||||
img {
|
||||
img:not(.channel-thumbnail__custom) {
|
||||
margin-bottom: var(--spacing-m);
|
||||
padding-top: var(--spacing-m);
|
||||
max-height: var(--inline-player-max-height);
|
||||
|
@ -178,9 +178,40 @@
|
|||
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 {
|
||||
padding: 0;
|
||||
margin: var(--spacing-s) 0;
|
||||
margin: 0;
|
||||
padding-right: var(--spacing-s);
|
||||
background-color: var(--color-primary-alt);
|
||||
display: block;
|
||||
|
@ -206,12 +237,21 @@
|
|||
.preview-link__thumbnail {
|
||||
width: 12rem;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.preview-link__thumbnail--channel {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.preview-link__description {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.file-viewer__embedded-header {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-preview {
|
||||
|
|
|
@ -298,7 +298,7 @@ textarea {
|
|||
.thumbnail-preview {
|
||||
width: var(--thumbnail-preview-width);
|
||||
height: var(--thumbnail-preview-height);
|
||||
|
||||
border-radius: var(--border-radius);
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
|
|
|
@ -38,6 +38,7 @@ $breakpoint-large: 1600px;
|
|||
--font-weight-bold: 700;
|
||||
--font-base: 14px;
|
||||
--font-body: 1rem;
|
||||
--font-xxsmall: 0.65rem;
|
||||
--font-xsmall: 0.7344rem;
|
||||
--font-small: 0.8571rem;
|
||||
--font-large: 1.3rem;
|
||||
|
@ -67,7 +68,9 @@ $breakpoint-large: 1600px;
|
|||
--tab-indicator-size: 0.5rem;
|
||||
|
||||
// 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-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"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#15737f9b098f3654122a75f93d63584e7ffa17a1:
|
||||
lbry-redux@lbryio/lbry-redux#823197af37da745fd79632b9e82deddf9d7fe033:
|
||||
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:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
uuid "^3.3.2"
|
||||
uuid "^8.3.1"
|
||||
|
||||
lbryinc@lbryio/lbryinc#db0663fcc4a64cb082b6edc5798fafa67eb4300f:
|
||||
version "0.0.1"
|
||||
|
@ -12130,6 +12130,11 @@ uuid@^8.3.0:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea"
|
||||
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:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
|
||||
|
|
Loading…
Reference in a new issue