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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(() => {
if (!hasResolvedUri) {
doResolveUri(uri);
}
if (!hasCostInfo) {
doFetchCostInfoForUri(uri);
}, [uri, doResolveUri, doFetchCostInfoForUri]);
}
}, [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>
);
}

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, {
'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()}

View file

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

View file

@ -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}`);
const element = mainFilePlaying
? document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`)
: document.querySelector(`.${INLINE_PLAYER_WRAPPER_CLASS}`);
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,
}
: {}
}
>

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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,6 +63,8 @@ class Spinner extends PureComponent<Props, State> {
}
return (
<>
{text}
<div
className={classnames('spinner', {
'spinner--dark': !light && (dark || theme === LIGHT_THEME),
@ -75,6 +78,7 @@ class Spinner extends PureComponent<Props, State> {
<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;
return renderMode === RENDER_MODES.MARKDOWN ? (
<Card body={<MarkdownPreview content={content} />} />
<Card body={<MarkdownPreview content={content} isMarkdownPost promptLinks />} />
) : (
<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_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';

View file

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

View file

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

View file

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

View file

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

View file

@ -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'
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;
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,19 +127,14 @@ 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 renderBlockedPage();
}
return (
<Page className="file-page" filePage>
<div className={classnames('section card-stack', `file-page__${renderMode}`)}>
{this.renderFilePageLayout(uri, renderMode, costInfo ? costInfo.cost : null)}
{renderFilePageLayout()}
<FileValues uri={uri} />
<FileDetails uri={uri} />
@ -153,6 +145,5 @@ class FilePage extends React.Component<Props> {
</Page>
);
}
}
export default FilePage;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@
.content__viewer--inline {
max-height: var(--inline-player-max-height);
border: none;
}
.content__viewer--floating {

View file

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

View file

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

View file

@ -14,7 +14,6 @@
.file-properties--large {
flex-wrap: wrap;
margin-bottom: var(--spacing-l);
margin-left: 0;
& > * {

View file

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

View file

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

View file

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

View file

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

View file

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