refactor floatingUri to allow inline players in comments/markdown

This commit is contained in:
Sean Yesmunt 2020-10-20 13:10:02 -04:00
parent 3b20104261
commit 295b8cf2e1
49 changed files with 540 additions and 331 deletions

View file

@ -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

View file

@ -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
View file

@ -0,0 +1,8 @@
// @flow
declare type PlayingUri = {
uri: string,
pathname: string,
commentId?: string,
source?: string,
};

View file

@ -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",

View file

@ -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",

View file

@ -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);

View file

@ -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>
);
} }
} }

View file

@ -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>
); );
}); });

View file

@ -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>

View file

@ -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),

View file

@ -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 (

View file

@ -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>
); );
} }

View file

@ -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,
}, },

View file

@ -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);

View file

@ -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>
); );
} }

View file

@ -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;

View file

@ -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()}

View file

@ -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));

View file

@ -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,
}
: {} : {}
} }
> >

View file

@ -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))));
}, },
}); });

View file

@ -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;

View file

@ -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),
}); });

View file

@ -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} />

View file

@ -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);

View 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;

View file

@ -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">

View file

@ -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>
</>
); );
} }
} }

View file

@ -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} />
); );

View file

@ -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';

View file

@ -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)),
}); });

View file

@ -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);
} }

View file

@ -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>

View file

@ -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));

View file

@ -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;

View file

@ -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),

View file

@ -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) {

View file

@ -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) => {

View file

@ -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) =>

View file

@ -526,3 +526,7 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }
} }
.claim-link {
position: relative;
}

View file

@ -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;
} }

View file

@ -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 {

View file

@ -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;

View file

@ -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)));
} }

View file

@ -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;
& > * { & > * {

View file

@ -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 {

View file

@ -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 {

View file

@ -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;

View file

@ -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);

View file

@ -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"