File downloads and refactoring #3918

Merged
kauffj merged 6 commits from file_downloads_and_refactoring into master 2020-04-01 20:43:50 +02:00
75 changed files with 1157 additions and 1194 deletions

View file

@ -68,7 +68,7 @@
"@babel/register": "^7.0.0",
"@exponent/electron-cookies": "^2.0.0",
"@hot-loader/react-dom": "^16.8",
"@lbry/components": "^3.0.12",
"@lbry/components": "^4.0.1",
"@reach/menu-button": "0.7.4",
"@reach/rect": "^0.2.1",
"@reach/tabs": "^0.1.5",

View file

@ -335,7 +335,6 @@
"credits": "credits",
"No channel name after @.": "No channel name after @.",
"View channel": "View channel",
"Add to your library": "Add to your library",
"Web link": "Web link",
"Facebook": "Facebook",
"Twitter": "Twitter",
@ -842,7 +841,6 @@
"Any amount will give you the highest bid, but larger amounts help your content be trusted and discovered.": "Any amount will give you the highest bid, but larger amounts help your content be trusted and discovered.",
"Loading 3D model.": "Loading 3D model.",
"Click here": "Click here",
"PDF opened externally. %click_here% to open it again.": "PDF opened externally. %click_here% to open it again.",
"Wallet Server": "Wallet Server",
"lbry.tv wallet servers": "lbry.tv wallet servers",
"Custom wallet servers": "Custom wallet servers",

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import IframeReact from './view';
const select = state => ({});
const perform = () => ({});
export default connect(select, perform)(IframeReact);

View file

@ -0,0 +1,35 @@
// @flow
import React from 'react';
type Props = {
fullHeight: boolean,
src: string,
title: string,
};
export default function I18nMessage(props: Props) {
kauffj commented 2020-03-31 03:15:37 +02:00 (Migrated from github.com)
Review

This doesn't do anything. If iframes are serving files from the same domain, growable iframes would work. Ironically we just changed this.

This doesn't do anything. If iframes are serving files from the same domain, growable iframes would work. Ironically we just changed this.
const { src, title } = props;
// const iframeRef = useRef();
// const [iframeHeight, setIframeHeight] = useState('80vh');
function onLoad() {
/*
iframe domain restrictions prevent naive design :-(
const obj = iframeRef.current;
if (obj) {
setIframeHeight(obj.contentWindow.document.body.scrollHeight + 'px');
}
*/
}
return (
// style={{height: iframeHeight}}
// ref={iframeRef}
<iframe src={src} title={title} onLoad={onLoad} />
);
}

View file

@ -10,7 +10,7 @@ import ReactModal from 'react-modal';
import { openContextMenu } from 'util/context-menu';
import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl';
import FloatingViewer from 'component/floatingViewer';
import FileRenderFloating from 'component/fileRenderFloating';
import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous';
import Nag from 'component/common/nag';
@ -286,7 +286,7 @@ function App(props: Props) {
<React.Fragment>
<Router />
<ModalRouter />
<FloatingViewer pageUri={uri} />
<FileRenderFloating pageUri={uri} />
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
{/* @if TARGET='app' */}

View file

@ -86,13 +86,14 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
const innerRef = useRef(null);
const combinedRef = useCombinedRefs(ref, innerRef, myref);
const size = iconSize || (!label && !children) ? 18 : undefined; // Fall back to default
const content = (
<span className="button__content">
{icon && <Icon icon={icon} iconColor={iconColor} size={iconSize} />}
{icon && <Icon icon={icon} iconColor={iconColor} size={size} />}
{label && <span className="button__label">{label}</span>}
{children && children}
{iconRight && <Icon icon={iconRight} iconColor={iconColor} size={iconSize} />}
{iconRight && <Icon icon={iconRight} iconColor={iconColor} size={size} />}
</span>
);

View file

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
import ClaimInsufficientCredits from './view';
const select = (state, props) => ({
isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
});
export default connect(select)(ClaimInsufficientCredits);

View file

@ -0,0 +1,33 @@
// @flow
import * as React from 'react';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
type Props = {
uri: string,
fileInfo: FileListItem,
isInsufficientCredits: boolean,
};
function ClaimInsufficientCredits(props: Props) {
const { isInsufficientCredits, fileInfo } = props;
if (fileInfo || !isInsufficientCredits) {
return null;
}
return (
<div className="media__insufficient-credits help--warning">
<I18nMessage
tokens={{
reward_link: <Button button="link" navigate="/$/rewards" label={__('Rewards')} />,
}}
>
The publisher has chosen to charge LBC to view this content. Your balance is currently too low to view it. Check
out %reward_link% for free LBC or send more LBC to your wallet.
</I18nMessage>
</div>
);
}
export default ClaimInsufficientCredits;

View file

@ -6,6 +6,7 @@ import classnames from 'classnames';
import ClaimPreview from 'component/claimPreview';
import Spinner from 'component/spinner';
import { FormField } from 'component/common/form';
import Card from 'component/common/card';
import usePersistedState from 'effects/use-persisted-state';
const SORT_NEW = 'new';
@ -161,9 +162,9 @@ export default function ClaimList(props: Props) {
</ul>
)}
{!timedOut && urisLength === 0 && !loading && (
<div className="card--section main--empty empty">{empty || __('No results')}</div>
<div className="empty empty--centered">{empty || __('No results')}</div>
)}
{timedOut && timedOutMessage && <div className="card--section main--empty empty">{timedOutMessage}</div>}
{timedOut && timedOutMessage && <div className="empty empty--centered">{timedOutMessage}</div>}
</section>
);
}

View file

@ -11,11 +11,10 @@ import {
selectBlockedChannels,
selectChannelIsBlocked,
doFileGet,
makeSelectStreamingUrlForUri,
} from 'lbry-redux';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectHasVisitedUri, makeSelectStreamingUrlForUriWebProxy } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import ClaimPreview from './view';
@ -34,7 +33,7 @@ const select = (state, props) => ({
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state),
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
});
const perform = dispatch => ({
@ -42,7 +41,4 @@ const perform = dispatch => ({
getFile: uri => dispatch(doFileGet(uri, false)),
});
export default connect(
select,
perform
)(ClaimPreview);
export default connect(select, perform)(ClaimPreview);

View file

@ -17,8 +17,8 @@ function ClaimUri(props: Props) {
return (
<Button
button="link"
className={classnames('media__uri', { 'media__uri--inline': inline })}
button="alt"
label={noShortUrl ? uri : shortUrl || uri}
onClick={() => {
clipboard.writeText(shortUrl || uri);

View file

@ -11,26 +11,47 @@ type Props = {
actions?: string | Node,
icon?: string,
className?: string,
isPageTitle?: boolean,
isBodyTable?: boolean,
actionIconPadding?: boolean,
};
export default function Card(props: Props) {
const { title, subtitle, body, actions, icon, className, actionIconPadding = true } = props;
const {
title,
subtitle,
body,
actions,
icon,
className,
isPageTitle = false,
isBodyTable = false,
kauffj commented 2020-03-31 03:16:14 +02:00 (Migrated from github.com)
Review

For cards that should have title as an h1

For cards that should have title as an h1
actionIconPadding = true,
kauffj commented 2020-03-31 03:16:34 +02:00 (Migrated from github.com)
Review

New option for cards where the entire body is a table (there was one on Help)

New option for cards where the entire body is a table (there was one on Help)
} = props;
return (
<section className={classnames(className, 'card')}>
{(title || subtitle) && (
<div className="card__header">
<div className="section__flex">
{icon && <Icon sectionIcon icon={icon} />}
<div>
<h2 className="section__title">{title}</h2>
{subtitle && <div className="section__subtitle">{subtitle}</div>}
</div>
{isPageTitle && <h1 className="card__title">{title}</h1>}
{!isPageTitle && <h2 className="card__title">{title}</h2>}
{subtitle && <div className="card__subtitle">{subtitle}</div>}
</div>
</div>
)}
{body && <div className={classnames('card__body', { 'card__body--with-icon': icon })}>{body}</div>}
{body && (
<div
className={classnames('card__body', {
'card__body--with-icon': icon,
'card__body--no-title': !title && !subtitle,
'card__body--table': isBodyTable,
})}
>
{body}
</div>
)}
{actions && (
<div
kauffj commented 2020-03-31 03:17:00 +02:00 (Migrated from github.com)
Review

add support cards without titles

add support cards without titles
className={classnames('card__main-actions', { 'card__main-actions--with-icon': icon && actionIconPadding })}

View file

@ -63,7 +63,7 @@ class IconComponent extends React.PureComponent<Props> {
color = this.getIconColor(iconColor);
}
const iconSize = size || 14;
const iconSize = size || 16;
let tooltipText;
if (tooltip) {

View file

@ -10,17 +10,24 @@ type Props = {
actionText: string,
href?: string,
type?: string,
inline?: boolean,
onClick?: () => void,
onClose?: () => void,
};
export default function Nag(props: Props) {
const { message, actionText, href, onClick, onClose, type } = props;
const { message, actionText, href, onClick, onClose, type, inline } = props;
const buttonProps = onClick ? { onClick } : { href };
return (
<div className={classnames('nag', { 'nag--helpful': type === 'helpful', 'nag--error': type === 'error' })}>
<div
className={classnames('nag', {
'nag--helpful': type === 'helpful',
'nag--error': type === 'error',
'nag--inline': inline,
})}
>
kauffj commented 2020-03-31 03:17:36 +02:00 (Migrated from github.com)
Review

Add support for inline nags (used on the player). Inline is possibly a bad term as I review this, should possibly be absolute?

Add support for inline nags (used on the player). Inline is possibly a bad term as I review this, should possibly be absolute?
<div className="nag__message">{message}</div>
<Button
className={classnames('nag__button', {

View file

@ -73,7 +73,7 @@ class ErrorBoundary extends React.Component<Props, State> {
if (hasError) {
return (
<div className="main main--empty">
<div className="main main--full-width main--empty">
<Yrbl
type="sad"
title={__('Aw shucks!')}

View file

@ -1,23 +1,18 @@
import { connect } from 'react-redux';
import * as SETTINGS from 'constants/settings';
import {
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
doPrepareEdit,
} from 'lbry-redux';
import { makeSelectClaimIsMine, makeSelectFileInfoForUri, makeSelectClaimForUri, doPrepareEdit } from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doOpenModal } from 'redux/actions/app';
import fs from 'fs';
import FilePage from './view';
import FileActions from './view';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
supportOption: makeSelectClientSetting(SETTINGS.SUPPORT_OPTION)(state),
});
@ -27,7 +22,4 @@ const perform = dispatch => ({
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
});
export default connect(
select,
perform
)(FilePage);
export default connect(select, perform)(FileActions);

View file

@ -1,12 +1,13 @@
// @flow
import type { Node } from 'react';
import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import React from 'react';
import Button from 'component/button';
import FileDownloadLink from 'component/fileDownloadLink';
import { buildURI } from 'lbry-redux';
import * as PAGES from '../../constants/pages';
import * as CS from '../../constants/claim_search';
import * as RENDER_MODES from 'constants/file_render_modes';
import useIsMobile from 'effects/use-is-mobile';
type Props = {
uri: string,
@ -16,14 +17,14 @@ type Props = {
claimIsMine: boolean,
fileInfo: FileListItem,
costInfo: ?{ cost: number },
contentType: string,
renderMode: string,
supportOption: boolean,
};
function FileActions(props: Props) {
const { fileInfo, uri, openModal, claimIsMine, claim, costInfo, contentType, supportOption, prepareEdit } = props;
const webShareable =
costInfo && costInfo.cost === 0 && contentType && ['video', 'image', 'audio'].includes(contentType.split('/')[0]);
const { fileInfo, uri, openModal, claimIsMine, claim, costInfo, renderMode, supportOption, prepareEdit } = props;
const isMobile = useIsMobile();
const webShareable = costInfo && costInfo.cost === 0 && RENDER_MODES.WEB_SHAREABLE_MODES.includes(renderMode);
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
const claimId = claim && claim.claim_id;
const { signing_channel: signingChannel } = claim;
@ -44,23 +45,16 @@ function FileActions(props: Props) {
editUri = buildURI(uriObject);
}
let repostLabel = <span>{__('Repost')}</span>;
if (claim.meta.reposted > 0) {
repostLabel = (
<Fragment>
{repostLabel}
<Button
button="alt"
label={__('(%count%)', { count: claim.meta.reposted })}
navigate={`/$/${PAGES.DISCOVER}?${CS.REPOSTED_URI_KEY}=${encodeURIComponent(uri)}`}
/>
</Fragment>
const ActionWrapper = (props: { children: Node }) =>
isMobile ? (
<React.Fragment>{props.children}</React.Fragment>
) : (
<div className="section__actions section__actions--no-margin">{props.children}</div>
);
}
return (
<div className="media__actions">
<div className="section__actions">
<ActionWrapper>
<Button
button="alt"
kauffj commented 2020-03-31 03:18:18 +02:00 (Migrated from github.com)
Review

🤮

🤮
icon={ICONS.SHARE}
@ -70,7 +64,7 @@ function FileActions(props: Props) {
<Button
button="alt"
icon={ICONS.REPOST}
label={repostLabel}
label={__('Repost %count%', { count: claim.meta.reposted > 0 ? `(${claim.meta.reposted})` : '' })}
requiresAuth={IS_WEB}
onClick={() => openModal(MODALS.REPOST, { uri })}
/>
@ -95,9 +89,9 @@ function FileActions(props: Props) {
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: true })}
/>
)}
</div>
</ActionWrapper>
<div className="section__actions">
<ActionWrapper>
<FileDownloadLink uri={uri} />
{claimIsMine && (
@ -129,7 +123,7 @@ function FileActions(props: Props) {
href={`https://lbry.com/dmca/${claimId}`}
/>
)}
</div>
</ActionWrapper>
</div>
);
}

View file

@ -5,6 +5,7 @@ import Button from 'component/button';
import Expandable from 'component/expandable';
import path from 'path';
import ClaimTags from 'component/claimTags';
import Card from 'component/common/card';
type Props = {
uri: string,
@ -42,6 +43,9 @@ class FileDetails extends PureComponent<Props> {
return (
<Fragment>
<Card
title={__('Details')}
body={
<Expandable>
{description && (
<div className="media__info-text">
@ -109,6 +113,8 @@ class FileDetails extends PureComponent<Props> {
</tbody>
</table>
</Expandable>
}
/>
</Fragment>
);
}

View file

@ -11,13 +11,14 @@ type Props = {
claimIsMine: boolean,
downloading: boolean,
loading: boolean,
isStreamable: boolean,
fileInfo: ?FileListItem,
openModal: (id: string, { path: string }) => void,
pause: () => void,
download: string => void,
triggerViewEvent: string => void,
costInfo: ?{ cost: string },
buttonType: ?string,
showLabel: ?boolean,
hideOpenButton: boolean,
hideDownloadStatus: boolean,
};
@ -35,6 +36,8 @@ function FileDownloadLink(props: Props) {
claim,
triggerViewEvent,
costInfo,
buttonType = 'alt',
showLabel = false,
hideOpenButton = false,
hideDownloadStatus = false,
} = props;
@ -73,10 +76,12 @@ function FileDownloadLink(props: Props) {
}
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
const openLabel = __('Open file');
return hideOpenButton ? null : (
<Button
button="alt"
title={__('Open file')}
button={buttonType}
title={openLabel}
label={showLabel ? openLabel : null}
icon={ICONS.EXTERNAL}
onClick={() => {
pause();
@ -86,11 +91,14 @@ function FileDownloadLink(props: Props) {
);
}
const label = IS_WEB ? __('Download') : __('Download to your Library');
return (
<Button
button="alt"
title={IS_WEB ? __('Download') : __('Add to your library')}
button={buttonType}
title={label}
icon={ICONS.DOWNLOAD}
label={showLabel ? label : null}
onClick={handleDownload}
// @if TARGET='web'
download={fileName}

View file

@ -3,14 +3,15 @@ import {
makeSelectClaimForUri,
makeSelectThumbnailForUri,
makeSelectContentTypeForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectDownloadPathForUri,
makeSelectFileNameForUri,
} from 'lbry-redux';
import * as SETTINGS from 'constants/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectIsText } from 'redux/selectors/content';
import {
makeSelectFileRenderModeForUri,
makeSelectFileExtensionForUri,
makeSelectStreamingUrlForUriWebProxy,
} from 'redux/selectors/content';
import { doSetPlayingUri } from 'redux/actions/content';
import FileRender from './view';
@ -19,14 +20,13 @@ const select = (state, props) => {
return {
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
claim: makeSelectClaimForUri(props.uri)(state),
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
downloadPath: makeSelectDownloadPathForUri(props.uri)(state),
fileName: makeSelectFileNameForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
fileExtension: makeSelectFileExtensionForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
autoplay: autoplay,
isText: makeSelectIsText(props.uri)(state),
};
};
@ -34,7 +34,4 @@ const perform = dispatch => ({
setPlayingUri: uri => dispatch(doSetPlayingUri(uri)),
});
export default connect(
select,
perform
)(FileRender);
export default connect(select, perform)(FileRender);

View file

@ -1,9 +1,9 @@
// @flow
import { URL } from 'config';
import { remote } from 'electron';
import React, { Suspense, Fragment } from 'react';
import React, { Suspense } from 'react';
import classnames from 'classnames';
import LoadingScreen from 'component/common/loading-screen';
import * as RENDER_MODES from 'constants/file_render_modes';
import VideoViewer from 'component/viewers/videoViewer';
import ImageViewer from 'component/viewers/imageViewer';
import AppViewer from 'component/viewers/appViewer';
@ -11,18 +11,14 @@ import Button from 'component/button';
import { withRouter } from 'react-router-dom';
import AutoplayCountdown from 'component/autoplayCountdown';
import { formatLbryUrlForWeb } from 'util/url';
// @if TARGET='web'
import { generateStreamUrl } from 'util/lbrytv';
// @endif
import path from 'path';
import fs from 'fs';
import Yrbl from 'component/yrbl';
import DocumentViewer from 'component/viewers/documentViewer';
import PdfViewer from 'component/viewers/pdfViewer';
kauffj commented 2020-03-31 03:31:03 +02:00 (Migrated from github.com)
Review

moved into proxied selector, this way we can assume when selecting a streamUrl it will just work

moved into proxied selector, this way we can assume when selecting a streamUrl it will just work
import HtmlViewer from 'component/viewers/htmlViewer';
// @if TARGET='app'
// should match
import DocxViewer from 'component/viewers/docxViewer';
import ComicBookViewer from 'component/viewers/comicBookViewer';
import ThreeViewer from 'component/viewers/threeViewer';
@ -30,18 +26,17 @@ import ThreeViewer from 'component/viewers/threeViewer';
type Props = {
uri: string,
mediaType: string,
isText: true,
streamingUrl: string,
embedded?: boolean,
contentType: string,
claim: StreamClaim,
currentTheme: string,
downloadPath: string,
fileName: string,
fileExtension: string,
autoplay: boolean,
setPlayingUri: (string | null) => void,
currentlyFloating: boolean,
renderMode: string,
thumbnail: string,
};
@ -113,114 +108,50 @@ class FileRender extends React.PureComponent<Props, State> {
}
renderViewer() {
const { mediaType, currentTheme, claim, contentType, downloadPath, fileName, streamingUrl, uri } = this.props;
const fileType = fileName && path.extname(fileName).substring(1);
const { currentTheme, contentType, downloadPath, fileExtension, streamingUrl, uri, renderMode } = this.props;
const source = streamingUrl;
// Ideally the lbrytv api server would just replace the streaming_url returned by the sdk so we don't need this check
// https://github.com/lbryio/lbrytv/issues/51
const source = IS_WEB ? generateStreamUrl(claim.name, claim.claim_id) : streamingUrl;
// Human-readable files (scripts and plain-text files)
const readableFiles = ['text', 'document', 'script'];
// Supported mediaTypes
const mediaTypes = {
// @if TARGET='app'
'3D-file': <ThreeViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
'comic-book': <ComicBookViewer source={{ fileType, downloadPath }} theme={currentTheme} />,
application: <AppViewer uri={uri} />,
// @endif
video: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
audio: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
image: <ImageViewer uri={uri} source={source} />,
// Add routes to viewer...
};
// Supported contentTypes
const contentTypes = {
'application/x-ext-mkv': (
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
),
'video/x-matroska': (
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
),
'application/pdf': <PdfViewer source={downloadPath || source} />,
'text/html': <HtmlViewer source={downloadPath || source} />,
'text/htm': <HtmlViewer source={downloadPath || source} />,
};
// Supported fileType
const fileTypes = {
// @if TARGET='app'
docx: <DocxViewer source={downloadPath} />,
// @endif
// Add routes to viewer...
};
// Check for a valid fileType, mediaType, or contentType
let viewer = (fileType && fileTypes[fileType]) || mediaTypes[mediaType] || contentTypes[contentType];
// Check for Human-readable files
if (!viewer && readableFiles.includes(mediaType)) {
viewer = (
switch (renderMode) {
case RENDER_MODES.AUDIO:
case RENDER_MODES.VIDEO:
return <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />;
case RENDER_MODES.IMAGE:
return <ImageViewer uri={uri} source={source} />;
case RENDER_MODES.HTML:
return <HtmlViewer source={downloadPath || source} />;
case RENDER_MODES.DOCUMENT:
case RENDER_MODES.MARKDOWN:
return (
<DocumentViewer
source={{
// @if TARGET='app'
file: options => fs.createReadStream(downloadPath, options),
// @endif
stream: source,
fileType,
fileExtension,
contentType,
}}
renderMode={renderMode}
theme={currentTheme}
/>
);
case RENDER_MODES.DOCX:
return <DocxViewer source={downloadPath} />;
case RENDER_MODES.PDF:
return <PdfViewer source={downloadPath || source} />;
case RENDER_MODES.CAD:
return <ThreeViewer source={{ fileExtension, downloadPath }} theme={currentTheme} />;
case RENDER_MODES.COMIC:
return <ComicBookViewer source={{ fileExtension, downloadPath }} theme={currentTheme} />;
case RENDER_MODES.APPLICATION:
return <AppViewer uri={uri} />;
}
// @if TARGET='web'
// temp workaround to disabled paid content on web
if (claim && claim.value.fee && Number(claim.value.fee.amount) > 0) {
const paidMessage = __(
'Currently, only free content is available on lbry.tv. Try viewing it in the desktop app.'
);
const paid = <LoadingScreen status={paidMessage} spinner={false} />;
return paid;
return null;
}
// @endif
const unsupported = IS_WEB ? (
<div className={'content__cover--disabled'}>
<Yrbl
className={'content__cover--disabled'}
title={'Not available on lbry.tv'}
subtitle={
<Fragment>
<p>
{__('Good news, though! You can')}{' '}
<Button button="link" label={__('Download the desktop app')} href="https://lbry.com/get" />{' '}
{'and have access to all file types.'}
</p>
</Fragment>
}
uri={uri}
/>
</div>
) : (
<div className={'content__cover--disabled'}>
<Yrbl
title={'Content Downloaded'}
subtitle={'This file is unsupported here, but you can view the content in an application of your choice'}
uri={uri}
/>
</div>
);
// Return viewer
return viewer || unsupported;
}
render() {
const { isText, uri, currentlyFloating, embedded } = this.props;
const { uri, currentlyFloating, embedded, renderMode } = this.props;
const { showAutoplayCountdown, showEmbededMessage } = this.state;
const lbrytvLink = `${URL}${formatLbryUrlForWeb(uri)}?src=embed`;
@ -228,7 +159,7 @@ class FileRender extends React.PureComponent<Props, State> {
<div
className={classnames({
'file-render': !embedded,
'file-render--document': isText && !embedded,
'file-render--document': RENDER_MODES.TEXT_MODES.includes(renderMode) && !embedded,
'file-render__embed': embedded,
})}
>

View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import { withRouter } from 'react-router';
import FileRenderDownload from './view';
const select = (state, props) => ({
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
});
export default withRouter(connect(select)(FileRenderDownload));

View file

@ -0,0 +1,43 @@
// @flow
import React from 'react';
kauffj commented 2020-03-31 03:31:58 +02:00 (Migrated from github.com)
Review

New view for downloadable content. Download only content does not go through <FileRenderInitiator> (previous ViewerInitiator)

New view for downloadable content. Download only content does not go through `<FileRenderInitiator>` (previous ViewerInitiator)
import FileDownloadLink from 'component/fileDownloadLink';
import * as RENDER_MODES from 'constants/file_render_modes';
import Card from 'component/common/card';
import Button from 'component/button';
type Props = {
uri: string,
isFree: boolean,
renderMode: string,
};
export default function FileRenderDownload(props: Props) {
const { uri, renderMode, isFree } = props;
// @if TARGET='web'
if (RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode)) {
return (
<Card
title={isFree ? __('Download or Get the App') : __('Get the App')}
subtitle={
<p>
{isFree
? __(
'This content can be downloaded from lbry.tv, but not displayed. It will display in LBRY Desktop, an app for desktop computers.'
)
: __('Paid content requires a full LBRY app.')}
</p>
}
actions={
<>
{isFree && <FileDownloadLink uri={uri} buttonType="primary" showLabel />}
<Button button={!isFree ? 'primary' : 'link'} label={__('Get the App')} href="https://lbry.com/get" />
</>
}
/>
);
}
// @endif
return <Card title={__('Download')} actions={<FileDownloadLink uri={uri} buttonType="primary" showLabel />} />;
}

View file

@ -1,42 +1,29 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import {
makeSelectFileInfoForUri,
makeSelectThumbnailForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectContentTypeForUri,
makeSelectUriIsStreamable,
makeSelectTitleForUri,
} from 'lbry-redux';
import { makeSelectFileInfoForUri, makeSelectTitleForUri } from 'lbry-redux';
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
import {
makeSelectIsPlaying,
makeSelectShouldObscurePreview,
selectPlayingUri,
makeSelectIsText,
makeSelectFileRenderModeForUri,
makeSelectStreamingUrlForUriWebProxy,
} from 'redux/selectors/content';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetPlayingUri } from 'redux/actions/content';
import { withRouter } from 'react-router';
import { doAnalyticsView } from 'redux/actions/app';
import FileViewer from './view';
import FileRenderFloating from './view';
const select = (state, props) => {
const uri = selectPlayingUri(state);
return {
uri,
title: makeSelectTitleForUri(uri)(state),
thumbnail: makeSelectThumbnailForUri(uri)(state),
mediaType: makeSelectMediaTypeForUri(uri)(state),
contentType: makeSelectContentTypeForUri(uri)(state),
fileInfo: makeSelectFileInfoForUri(uri)(state),
obscurePreview: makeSelectShouldObscurePreview(uri)(state),
isPlaying: makeSelectIsPlaying(uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
isStreamable: makeSelectUriIsStreamable(uri)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(uri)(state),
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
isText: makeSelectIsText(uri)(state),
renderMode: makeSelectFileRenderModeForUri(uri)(state),
};
};
@ -46,9 +33,4 @@ const perform = dispatch => ({
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
});
export default withRouter(
connect(
select,
perform
)(FileViewer)
);
export default withRouter(connect(select, perform)(FileRenderFloating));

View file

@ -1,5 +1,6 @@
// @flow
import * as ICONS from 'constants/icons';
import * as RENDER_MODES from 'constants/file_render_modes';
import React, { useState, useEffect } from 'react';
import Button from 'component/button';
import classnames from 'classnames';
@ -8,24 +9,17 @@ import FileRender from 'component/fileRender';
import UriIndicator from 'component/uriIndicator';
import usePersistedState from 'effects/use-persisted-state';
import usePrevious from 'effects/use-previous';
import { FILE_WRAPPER_CLASS } from 'component/layoutWrapperFile/view';
import { FILE_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-is-mobile';
type Props = {
mediaType: string,
contentType: string,
isText: boolean,
isLoading: boolean,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
obscurePreview: boolean,
insufficientCredits: boolean,
isStreamable: boolean,
thumbnail?: string,
streamingUrl?: string,
floatingPlayer: boolean,
pageUri: ?string,
@ -33,25 +27,23 @@ type Props = {
floatingPlayerEnabled: boolean,
clearPlayingUri: () => void,
triggerAnalyticsView: (string, number) => Promise<any>,
renderMode: string,
claimRewards: () => void,
};
export default function FileViewer(props: Props) {
export default function FloatingViewer(props: Props) {
const {
isPlaying,
fileInfo,
uri,
streamingUrl,
isStreamable,
pageUri,
title,
clearPlayingUri,
floatingPlayerEnabled,
triggerAnalyticsView,
claimRewards,
mediaType,
contentType,
isText,
renderMode,
} = props;
const isMobile = useIsMobile();
const [playTime, setPlayTime] = useState();
@ -60,40 +52,17 @@ export default function FileViewer(props: Props) {
x: -25,
y: window.innerHeight - 400,
});
const inline = pageUri === uri;
const forceVideo = ['application/x-ext-mkv', 'video/x-matroska'].includes(contentType);
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const isReadyToPlay =
(IS_WEB && (isStreamable || webStreamOnly || forceVideo)) ||
((isStreamable || forceVideo) && streamingUrl) ||
(fileInfo && fileInfo.completed);
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
const loadingMessage =
!isStreamable && fileInfo && fileInfo.blobs_completed >= 1 && (!fileInfo.download_path || !fileInfo.written_bytes)
fileInfo && fileInfo.blobs_completed >= 1 && (!fileInfo.download_path || !fileInfo.written_bytes)
? __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.")
: __('Loading');
const previousUri = usePrevious(uri);
const isNewView = uri && previousUri !== uri && isPlaying;
const [hasRecordedView, setHasRecordedView] = useState(false);
useEffect(() => {
if (isNewView) {
setPlayTime(Date.now());
}
}, [isNewView, uri]);
useEffect(() => {
if (playTime && isReadyToPlay && !hasRecordedView) {
const timeToStart = Date.now() - playTime;
triggerAnalyticsView(uri, timeToStart).then(() => {
claimRewards();
setHasRecordedView(false); // This is a terrible variable name, rename this
setPlayTime(null);
});
}
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
useEffect(() => {
function handleResize() {
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
@ -115,6 +84,27 @@ export default function FileViewer(props: Props) {
};
}, [setFileViewerRect, inline]);
useEffect(() => {
if (isNewView) {
setPlayTime(Date.now());
}
}, [isNewView, uri]);
useEffect(() => {
if (playTime && isReadyToPlay && !hasRecordedView) {
const timeToStart = Date.now() - playTime;
triggerAnalyticsView(uri, timeToStart).then(() => {
claimRewards();
setHasRecordedView(false); // This is a terrible variable name, rename this
setPlayTime(null);
});
}
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
if (!isPlayable || !isPlaying || !uri || (!inline && (isMobile || !floatingPlayerEnabled))) {
return null;
}
function handleDrag(e, ui) {
const { x, y } = position;
const newX = x + ui.deltaX;
@ -125,16 +115,6 @@ export default function FileViewer(props: Props) {
});
}
const hidePlayer =
isText ||
!isPlaying ||
!uri ||
(!inline && (isMobile || !floatingPlayerEnabled || !['audio', 'video'].includes(mediaType)));
if (hidePlayer) {
return null;
}
return (
<Draggable
onDrag={handleDrag}

View file

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'lbry-redux';
import ClaimUri from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
});
export default connect(select)(ClaimUri);

View file

@ -0,0 +1,32 @@
// @flow
import * as PAGES from 'constants/pages';
import * as CS from 'constants/claim_search';
import React from 'react';
import ClaimUri from 'component/claimUri';
import Button from 'component/button';
type Props = {
uri: string,
claim: ?Claim,
};
function FileRenderHeader(props: Props) {
const { uri, claim } = props;
return (
<div>
<ClaimUri uri={uri} />
{claim.meta.reposted > 0 && (
<Button
button="link"
className="media__uri--right"
label={__('View %count% reposts', { count: claim.meta.reposted })}
navigate={`/$/${PAGES.DISCOVER}?${CS.REPOSTED_URI_KEY}=${encodeURIComponent(uri)}`}
/>
)}
</div>
);
}
export default FileRenderHeader;

View file

@ -1,41 +1,32 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
import {
makeSelectFileInfoForUri,
makeSelectThumbnailForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectContentTypeForUri,
makeSelectUriIsStreamable,
makeSelectClaimForUri,
} from 'lbry-redux';
import { makeSelectFileInfoForUri, makeSelectThumbnailForUri, makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { withRouter } from 'react-router';
import {
makeSelectIsPlaying,
makeSelectShouldObscurePreview,
selectPlayingUri,
makeSelectCanAutoplay,
makeSelectIsText,
makeSelectInsufficientCreditsForUri,
makeSelectStreamingUrlForUriWebProxy,
makeSelectFileRenderModeForUri,
} from 'redux/selectors/content';
import FileViewer from './view';
import FileRenderInitiator from './view';
const select = (state, props) => ({
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
isPlaying: makeSelectIsPlaying(props.uri)(state),
playingUri: selectPlayingUri(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
isAutoPlayable: makeSelectCanAutoplay(props.uri)(state),
isText: makeSelectIsText(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
});
@ -48,7 +39,4 @@ const perform = dispatch => ({
},
});
export default connect(
select,
perform
)(FileViewer);
export default withRouter(connect(select, perform)(FileRenderInitiator));

View file

@ -5,78 +5,52 @@
// while a file is currently being viewed
import React, { useEffect, useCallback } from 'react';
import classnames from 'classnames';
import * as PAGES from 'constants/pages';
import * as RENDER_MODES from 'constants/file_render_modes';
import Button from 'component/button';
import isUserTyping from 'util/detect-typing';
import Yrbl from 'component/yrbl';
import I18nMessage from 'component/i18nMessage';
import { generateDownloadUrl } from 'util/lbrytv';
import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
import Nag from 'component/common/nag';
const SPACE_BAR_KEYCODE = 32;
type Props = {
play: string => void,
mediaType: string,
isText: boolean,
contentType: string,
isLoading: boolean,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
history: { push: string => void },
obscurePreview: boolean,
insufficientCredits: boolean,
isStreamable: boolean,
thumbnail?: string,
autoplay: boolean,
hasCostInfo: boolean,
costInfo: any,
isAutoPlayable: boolean,
inline: boolean,
renderMode: string,
claim: StreamClaim,
};
export default function FileViewerInitiator(props: Props) {
export default function FileRenderInitiator(props: Props) {
const {
play,
mediaType,
isText,
contentType,
isPlaying,
fileInfo,
uri,
obscurePreview,
insufficientCredits,
history,
thumbnail,
autoplay,
isStreamable,
renderMode,
hasCostInfo,
costInfo,
isAutoPlayable,
claim,
} = props;
const cost = costInfo && costInfo.cost;
const forceVideo = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
const isPlayable = ['audio', 'video'].includes(mediaType) || forceVideo;
const isImage = mediaType === 'image';
const isFree = hasCostInfo && cost === 0;
const fileStatus = fileInfo && fileInfo.status;
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const supported = IS_WEB ? (!cost && isStreamable) || webStreamOnly || forceVideo : true;
const { name, claim_id: claimId, value } = claim;
const fileName = value && value.source && value.source.name;
const downloadUrl = generateDownloadUrl(name, claimId);
function getTitle() {
let message = __('Unsupported File');
// @if TARGET='web'
if (cost) {
message = __('Paid Content Not Supported on lbry.tv');
} else {
message = __("We're not quite ready to display this file on lbry.tv yet");
}
// @endif
return message;
}
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
// Wrap this in useCallback because we need to use it to the keyboard effect
// If we don't a new instance will be created for every render and react will think the dependencies have changed, which will add/remove the listener for every render
@ -112,45 +86,51 @@ export default function FileViewerInitiator(props: Props) {
useEffect(() => {
const videoOnPage = document.querySelector('video');
if (((autoplay && !videoOnPage && isAutoPlayable) || isText || isImage) && hasCostInfo && cost === 0) {
if (isFree && ((autoplay && !videoOnPage && isPlayable) || RENDER_MODES.AUTO_RENDER_MODES.includes(renderMode))) {
viewFile();
}
}, [autoplay, viewFile, isAutoPlayable, hasCostInfo, cost, isText, isImage]);
}, [autoplay, viewFile, isFree, renderMode]);
/*
once content is playing, let the appropriate <FileRender> take care of it...
but for playables, always render so area can be used to fill with floating player
*/
if (isPlaying && !isPlayable) {
return null;
}
const showAppNag = IS_WEB && (!isFree || RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode));
const disabled = showAppNag || (!fileInfo && insufficientCredits);
return (
<div
disabled={!hasCostInfo}
style={!obscurePreview && supported && thumbnail && !isPlaying ? { backgroundImage: `url("${thumbnail}")` } : {}}
onClick={supported ? viewFile : undefined}
className={classnames({
content__cover: supported,
'content__cover--disabled': !supported,
'content__cover--hidden-for-text': isText,
onClick={disabled ? undefined : viewFile}
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', {
'content__cover--disabled': disabled,
'card__media--nsfw': obscurePreview,
'card__media--disabled': supported && !fileInfo && insufficientCredits,
})}
>
{!supported && (
<Yrbl
type="happy"
title={getTitle()}
subtitle={
<I18nMessage
tokens={{
download_the_app: <Button button="link" label={__('download the app')} href="https://lbry.com/get" />,
download_this_file: (
<Button button="link" label={__('download this file')} download={fileName} href={downloadUrl} />
),
}}
>
Good news, though! You can %download_the_app% and gain access to everything, or %download_this_file% and
view it on your device.
</I18nMessage>
}
{showAppNag && (
<Nag
type="helpful"
inline
message={__('This content requires LBRY Desktop to display.')}
actionText={__('Get the App')}
href="https://lbry.com/get"
/>
)}
{!isPlaying && supported && (
{insufficientCredits && !showAppNag && (
<Nag
type="helpful"
inline
message={__('You need more credits to purchase this.')}
actionText={__('Open Rewards')}
onClick={() => history.push(`/$/${PAGES.REWARDS}`)}
/>
)}
{!disabled && (
<Button
onClick={viewFile}
iconSize={30}

View file

@ -0,0 +1,25 @@
import { connect } from 'react-redux';
kauffj commented 2020-03-31 03:38:14 +02:00 (Migrated from github.com)
Review

"TextViewer" is now FileRenderInline

This is used for all content types that are renderable (i.e. not downloadable) but not "playable" (go into floating player)

"TextViewer" is now FileRenderInline This is used for all content types that are renderable (i.e. not downloadable) but not "playable" (go into floating player)
import { makeSelectFileInfoForUri } from 'lbry-redux';
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
import {
makeSelectFileRenderModeForUri,
makeSelectIsPlaying,
makeSelectStreamingUrlForUriWebProxy,
} 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),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
});
const perform = dispatch => ({
triggerAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
});
export default withRouter(connect(select, perform)(FileRenderInline));

View file

@ -1,39 +1,27 @@
// @flow
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import FileRender from 'component/fileRender';
import usePrevious from 'effects/use-previous';
import LoadingScreen from 'component/common/loading-screen';
type Props = {
mediaType: string,
contentType: string,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
isStreamable: boolean,
renderMode: string,
streamingUrl?: string,
triggerAnalyticsView: (string, number) => Promise<any>,
claimRewards: () => void,
};
export default function TextViewer(props: Props) {
const {
isPlaying,
fileInfo,
uri,
streamingUrl,
isStreamable,
triggerAnalyticsView,
claimRewards,
mediaType,
contentType,
} = props;
export default function FileRenderInline(props: Props) {
const { isPlaying, fileInfo, uri, streamingUrl, triggerAnalyticsView, claimRewards } = props;
const [playTime, setPlayTime] = useState();
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const previousUri = usePrevious(uri);
const isNewView = uri && previousUri !== uri && isPlaying;
const [hasRecordedView, setHasRecordedView] = useState(false);
const isReadyToPlay = (IS_WEB && (isStreamable || streamingUrl || webStreamOnly)) || (fileInfo && fileInfo.completed);
const isReadyToPlay = streamingUrl || (fileInfo && fileInfo.completed);
useEffect(() => {
if (isNewView) {
@ -52,9 +40,9 @@ export default function TextViewer(props: Props) {
}
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
return (
<div className={classnames('content__viewersss')}>
{isReadyToPlay ? <FileRender uri={uri} /> : <div className="placeholder--text-document" />}
</div>
);
if (!isPlaying) {
return null;
}
return isReadyToPlay ? <FileRender uri={uri} /> : <LoadingScreen status={__('Preparing your content')} />;
}

View file

@ -0,0 +1,11 @@
import { connect } from 'react-redux';
import { makeSelectTitleForUri } from 'lbry-redux';
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
import FileTitle from './view';
const select = (state, props) => ({
isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
});
export default connect(select)(FileTitle);

View file

@ -0,0 +1,46 @@
// @flow
import * as React from 'react';
import { normalizeURI } from 'lbry-redux';
import FilePrice from 'component/filePrice';
import ClaimInsufficientCredits from 'component/claimInsufficientCredits';
import FileSubtitle from 'component/fileSubtitle';
import FileAuthor from 'component/fileAuthor';
import FileActions from 'component/fileActions';
import Card from 'component/common/card';
type Props = {
uri: string,
title: string,
nsfw: boolean,
};
function FileTitle(props: Props) {
const { title, uri, nsfw } = props;
return (
<Card
isPageTitle
title={
<React.Fragment>
{title}
<FilePrice badge uri={normalizeURI(uri)} />
{nsfw && (
<span className="media__title-badge">
<span className="badge badge--tag-mature">{__('Mature')}</span>
</span>
)}
</React.Fragment>
}
body={
<React.Fragment>
<ClaimInsufficientCredits uri={uri} />
<FileSubtitle uri={uri} />
<FileAuthor uri={uri} />
</React.Fragment>
}
actions={<FileActions uri={uri} />}
/>
);
}
export default FileTitle;

View file

@ -1,9 +1,15 @@
import { connect } from 'react-redux';
import { makeSelectViewCountForUri } from 'lbryinc';
import { doFetchViewCount, makeSelectViewCountForUri } from 'lbryinc';
import FileViewCount from './view';
import { makeSelectClaimForUri } from 'lbry-redux';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
viewCount: makeSelectViewCountForUri(props.uri)(state),
});
export default connect(select)(FileViewCount);
const perform = dispatch => ({
fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)),
});
export default connect(select, perform)(FileViewCount);

View file

@ -1,13 +1,22 @@
// @flow
import React from 'react';
import React, { useEffect } from 'react';
import HelpLink from 'component/common/help-link';
type Props = {
claim: StreamClaim,
fetchViewCount: string => void,
uri: string,
viewCount: string,
};
function FileViewCount(props: Props) {
const { viewCount } = props;
const { claim, uri, fetchViewCount, viewCount } = props;
useEffect(() => {
if (claim && claim.claim_id) {
fetchViewCount(claim.claim_id);
}
}, [fetchViewCount, uri, claim]);
return (
<span>

View file

@ -1,35 +0,0 @@
import { connect } from 'react-redux';
import * as SETTINGS from 'constants/settings';
import {
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
doPrepareEdit,
makeSelectTitleForUri,
} from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doOpenModal } from 'redux/actions/app';
import fs from 'fs';
import FilePage from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
supportOption: makeSelectClientSetting(SETTINGS.SUPPORT_OPTION)(state),
title: makeSelectTitleForUri(props.uri)(state),
});
const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
});
export default connect(
select,
perform
)(FilePage);

View file

@ -1,84 +0,0 @@
// @flow
import * as React from 'react';
import { normalizeURI } from 'lbry-redux';
import FileViewerInitiator from 'component/fileViewerInitiator';
import FileSubtitle from 'component/fileSubtitle';
import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails';
import FileAuthor from 'component/fileAuthor';
import FileActions from 'component/fileActions';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
import ClaimUri from 'component/claimUri';
export const FILE_WRAPPER_CLASS = 'grid-area--content';
type Props = {
claim: StreamClaim,
fileInfo: FileListItem,
uri: string,
claimIsMine: boolean,
costInfo: ?{ cost: number },
balance: number,
title: string,
nsfw: boolean,
};
function LayoutWrapperFile(props: Props) {
const { claim, uri, claimIsMine, costInfo, balance, title, nsfw } = props;
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
return (
<div>
<ClaimUri uri={uri} />
<div className={`card ${FILE_WRAPPER_CLASS}`}>
<FileViewerInitiator uri={uri} insufficientCredits={insufficientCredits} />
</div>
<div className="media__title">
<span className="media__title-badge">
{nsfw && <span className="badge badge--tag-mature">{__('Mature')}</span>}
</span>
<span className="media__title-badge">
<FilePrice badge uri={normalizeURI(uri)} />
</span>
<h1 className="media__title-text">{title}</h1>
</div>
<div className="columns">
<div className="grid-area--info">
<FileSubtitle uri={uri} />
<FileActions uri={uri} />
<div className="section__divider">
<hr />
</div>
<FileAuthor uri={uri} />
<div className="section">
<FileDetails uri={uri} />
</div>
<div className="section__divider">
<hr />
</div>
<div className="section__title--small">{__('Comments')}</div>
<section className="section">
<CommentCreate uri={uri} />
</section>
<section className="section">
<CommentsList uri={uri} />
</section>
</div>
<div className="grid-area--related">
<RecommendedContent uri={uri} claimId={claim.claim_id} />
</div>
</div>
</div>
);
}
export default LayoutWrapperFile;

View file

@ -1,35 +0,0 @@
import { connect } from 'react-redux';
import * as SETTINGS from 'constants/settings';
import {
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
doPrepareEdit,
makeSelectTitleForUri,
} from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doOpenModal } from 'redux/actions/app';
import fs from 'fs';
import LayoutWrapperNonDocument from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
supportOption: makeSelectClientSetting(SETTINGS.SUPPORT_OPTION)(state),
title: makeSelectTitleForUri(props.uri)(state),
});
const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
});
export default connect(
select,
perform
)(LayoutWrapperNonDocument);

View file

@ -1,90 +0,0 @@
// @flow
import * as React from 'react';
import { normalizeURI } from 'lbry-redux';
import classNames from 'classnames';
import FileSubtitle from 'component/fileSubtitle';
import FilePrice from 'component/filePrice';
import FileAuthor from 'component/fileAuthor';
import FileActions from 'component/fileActions';
import FileDetails from 'component/fileDetails';
import TextViewer from 'component/textViewer';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
import ClaimUri from 'component/claimUri';
import FileViewerInitiator from 'component/fileViewerInitiator';
type Props = {
uri: string,
title: string,
nsfw: boolean,
claim: StreamClaim,
thumbnail: ?string,
contentType: string,
fileType: string,
};
function LayoutWrapperText(props: Props) {
const { uri, claim, title, nsfw, contentType, fileType } = props;
const markdownType = ['md', 'markdown'];
const isMarkdown = markdownType.includes(fileType) || contentType === 'text/markdown' || contentType === 'text/md';
return (
<div>
<div className={classNames('main__document-wrapper', { 'main__document-wrapper--markdown': isMarkdown })}>
<ClaimUri uri={uri} />
<div className="media__title">
<span className="media__title-badge">
{nsfw && <span className="badge badge--tag-mature">{__('Mature')}</span>}
</span>
<span className="media__title-badge">
<FilePrice badge uri={normalizeURI(uri)} />
</span>
<h1 className="media__title-text">{title}</h1>
</div>
<FileSubtitle uri={uri} />
<div className="section">
<FileAuthor uri={uri} />
</div>
<div className="section__divider">
<hr />
</div>
{/* Render the initiator to trigger the view of the file */}
<FileViewerInitiator uri={uri} />
<TextViewer uri={uri} />
</div>
<div className="columns">
<div>
<FileActions uri={uri} />
<div className="section__divider">
<hr />
</div>
<FileAuthor uri={uri} />
<div className="section">
<FileDetails uri={uri} />
</div>
<div className="section__title--small">{__('Comments')}</div>
<section className="section">
<CommentCreate uri={uri} />
</section>
<section className="section">
<CommentsList uri={uri} />
</section>
</div>
<RecommendedContent uri={uri} claimId={claim.claim_id} />
</div>
</div>
);
}
export default LayoutWrapperText;

View file

@ -7,7 +7,7 @@
On web, the Lbry publish method call is overridden in platform/web/api-setup, using a function in platform/web/publish.
File upload is carried out in the background by that function.
*/
import React, { useEffect, Fragment } from 'react';
import React, { useEffect } from 'react';
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
import Button from 'component/button';
@ -141,7 +141,7 @@ function PublishForm(props: Props) {
}, [name, channel, resolveUri, updatePublishForm]);
return (
<Fragment>
<div className="card-stack">
<PublishFile disabled={disabled || publishing} inProgress={isInProgress} />
{!publishing && (
<div className={classnames({ 'card--disabled': formDisabled })}>
@ -209,7 +209,7 @@ function PublishForm(props: Props) {
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
</p>
</section>
</Fragment>
</div>
);
}

View file

@ -11,7 +11,6 @@ type Options = {
type Props = {
uri: string,
claim: ?StreamClaim,
claimId: string,
recommendedContent: Array<string>,
isSearching: boolean,
search: (string, Options) => void,
@ -43,10 +42,10 @@ export default class RecommendedContent extends React.PureComponent<Props> {
}
getRecommendedContent() {
const { claim, search, mature, claimId } = this.props;
const { claim, search, mature } = this.props;
if (claim && claim.value && claim.value) {
const options: Options = { size: 20, related_to: claimId, isBackgroundSearch: true };
if (claim && claim.value && claim.claim_id) {
const options: Options = { size: 20, related_to: claim.claim_id, isBackgroundSearch: true };
if (claim && !mature) {
options['nsfw'] = false;
}

View file

@ -1,34 +0,0 @@
import { connect } from 'react-redux';
import {
makeSelectFileInfoForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
makeSelectContentTypeForUri,
makeSelectUriIsStreamable,
} from 'lbry-redux';
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
import { makeSelectIsPlaying } from 'redux/selectors/content';
import { withRouter } from 'react-router';
import { doAnalyticsView } from 'redux/actions/app';
import FileViewer from './view';
const select = (state, props) => ({
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
isPlaying: makeSelectIsPlaying(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
});
const perform = dispatch => ({
triggerAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
});
export default withRouter(
connect(
select,
perform
)(FileViewer)
);

View file

@ -39,7 +39,7 @@ function AppViewer(props: Props) {
// }, [outpoint, contentType, setAppUrl, setLoading]);
return (
<div className="content__cover--disabled">
<div className="content__cover--none">
<Yrbl
title={__('Sorry')}
subtitle={__('Games and apps are currently disabled due to potential security concerns.')}

View file

@ -3,11 +3,14 @@
import React from 'react';
import LoadingScreen from 'component/common/loading-screen';
import MarkdownPreview from 'component/common/markdown-preview';
import Card from 'component/common/card';
import CodeViewer from 'component/viewers/codeViewer';
import * as RENDER_MODES from 'constants/file_render_modes';
import * as https from 'https';
type Props = {
theme: string,
renderMode: string,
source: {
file: (?string) => any,
stream: string,
@ -79,20 +82,15 @@ class DocumentViewer extends React.PureComponent<Props, State> {
}
renderDocument() {
let viewer = null;
const { content } = this.state;
const { source, theme } = this.props;
const { fileType, contentType } = source;
const markdownType = ['md', 'markdown'];
if (markdownType.includes(fileType) || contentType === 'text/markdown' || contentType === 'text/md') {
// Render markdown
viewer = <MarkdownPreview content={content} />;
} else {
// Render plain text
viewer = <CodeViewer value={content} contentType={contentType} theme={theme} />;
}
const { source, theme, renderMode } = this.props;
const { contentType } = source;
return viewer;
return renderMode === RENDER_MODES.MARKDOWN ? (
<Card body={<MarkdownPreview content={content} />} />
) : (
<CodeViewer value={content} contentType={contentType} theme={theme} />
);
}
render() {
@ -101,7 +99,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
const errorMessage = __("Sorry, looks like we can't load the document.");
return (
<div className="file-render__viewer--document">
<div className="file-render__viewer file-render__viewer--document">
{loading && !error && <div className="placeholder--text-document" />}
{error && <LoadingScreen status={errorMessage} spinner={!error} />}
{isReady && this.renderDocument()}

View file

@ -58,7 +58,7 @@ class DocxViewer extends React.PureComponent<Props, State> {
const errorMessage = __("Sorry, looks like we can't load the document.");
return (
<div className="file-render__viewer--document">
<div className="file-render__viewer file-render__viewer--document">
{loading && <LoadingScreen status={loadingMessage} spinner />}
{error && <LoadingScreen status={errorMessage} spinner={false} />}
{content && <div className="file-render__content" dangerouslySetInnerHTML={{ __html: content }} />}

View file

@ -35,7 +35,10 @@ class HtmlViewer extends React.PureComponent<Props, State> {
const { source } = this.props;
const { loading } = this.state;
return (
<div className="file-render__viewer" onContextMenu={stopContextMenu}>
<div
className="file-render__viewer file-render__viewer--html file-render__viewer--iframe"
onContextMenu={stopContextMenu}
>
{loading && <div className="placeholder--text-document" />}
{/* @if TARGET='app' */}
<iframe ref={this.iframe} hidden={loading} sandbox="" title={__('File preview')} src={`file://${source}`} />

View file

@ -1,57 +1,21 @@
// @flow
import * as React from 'react';
import { stopContextMenu } from 'util/context-menu';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
// @if TARGET='app'
import { shell } from 'electron';
// @endif
import IframeReact from 'component/IframeReact';
type Props = {
source: string,
};
class PdfViewer extends React.PureComponent<Props> {
constructor() {
super();
(this: any).openFile = this.openFile.bind(this);
}
componentDidMount() {
this.openFile();
}
openFile() {
const { source } = this.props;
const path = `file://${source}`;
// @if TARGET='app'
shell.openExternal(path);
// @endif
}
render() {
// We used to be able to just render a webview and display the pdf inside the app
// This was disabled on electron@3
// https://github.com/electron/electron/issues/12337
const { source } = this.props;
const src = IS_WEB ? source : `file://${source}`;
return (
<div className="file-render__viewer--pdf" onContextMenu={stopContextMenu}>
{/* @if TARGET='app' */}
<p>
<I18nMessage
tokens={{ click_here: <Button button="link" label={__('Click here')} onClick={this.openFile} /> }}
>
PDF opened externally. %click_here% to open it again.
</I18nMessage>
</p>
{/* @endif */}
{/* @if TARGET='web' */}
<div className="file-render__viewer">
<iframe title={__('File preview')} src={source} />
<div className="file-render__viewer file-render__viewer--document" onContextMenu={stopContextMenu}>
<div className="file-render__viewer file-render__viewer--iframe">
<IframeReact title={__('File preview')} src={src} />
</div>
{/* @endif */}
</div>
);
}

View file

@ -23,7 +23,7 @@ const WalletBalance = (props: Props) => {
return (
<React.Fragment>
<section className="section__flex-wrap">
<section className="columns">
<div>
<h2 className="section__title">{__('Available Balance')}</h2>
<span className="section__title--large">

View file

@ -0,0 +1,32 @@
export const VIDEO = 'video';
export const AUDIO = 'audio';
export const FLOATING_MODES = [VIDEO, AUDIO]; // these types will show in floating player
export const PDF = 'pdf';
export const DOCX = 'docx';
export const HTML = 'html';
export const MARKDOWN = 'md';
export const DOCUMENT = 'document';
export const PLAIN_TEXT = 'plain_text';
export const TEXT_MODES = [PDF, DOCUMENT, PLAIN_TEXT, DOCX, HTML, MARKDOWN]; // these types will use text/document layout
export const IMAGE = 'image';
export const CAD = 'cad';
export const COMIC = 'comic';
export const AUTO_RENDER_MODES = [IMAGE].concat(TEXT_MODES); // these types will render (and thus download) automatically (if free)
export const WEB_SHAREABLE_MODES = AUTO_RENDER_MODES.concat(FLOATING_MODES);
export const DOWNLOAD = 'download';
export const APPLICATION = 'application';
export const UNSUPPORTED = 'unsupported';
// PDFs disabled on desktop until we update Electron: https://github.com/electron/electron/issues/12337
// Comics disabled because nothing is actually reporting as a comic type
export const UNSUPPORTED_IN_THIS_APP = IS_WEB ? [CAD, COMIC, APPLICATION] : [CAD, COMIC, APPLICATION, PDF];
export const UNRENDERABLE_MODES = Array.from(
new Set(UNSUPPORTED_IN_THIS_APP.concat([DOWNLOAD, APPLICATION, UNSUPPORTED]))
);

View file

@ -3,17 +3,15 @@ import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
import { doSetContentHistoryItem } from 'redux/actions/content';
import {
doFetchFileInfo,
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectMetadataForUri,
makeSelectChannelForClaimUri,
selectBalance,
} from 'lbry-redux';
import { doFetchViewCount, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { makeSelectIsText } from 'redux/selectors/content';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import FilePage from './view';
const select = (state, props) => ({
@ -22,11 +20,9 @@ const select = (state, props) => ({
metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowMatureContent(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
balance: selectBalance(state),
isText: makeSelectIsText(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
});
const perform = dispatch => ({
@ -34,10 +30,6 @@ const perform = dispatch => ({
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)),
});
export default connect(
select,
perform
)(FilePage);
export default connect(select, perform)(FilePage);

View file

@ -1,41 +1,45 @@
// @flow
import * as React from 'react';
import Button from 'component/button';
import classnames from 'classnames';
import Page from 'component/page';
import I18nMessage from 'component/i18nMessage/view';
import LayoutWrapperFile from 'component/layoutWrapperFile';
import LayoutWrapperText from 'component/layoutWrapperText';
import * as RENDER_MODES from 'constants/file_render_modes';
import FileRenderHeader from 'component/fileRenderHeader';
import FileTitle from 'component/fileTitle';
import FileRenderInitiator from 'component/fileRenderInitiator';
import FileRenderInline from 'component/fileRenderInline';
import FileRenderDownload from 'component/fileRenderDownload';
import Card from 'component/common/card';
import FileDetails from 'component/fileDetails';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
export const FILE_WRAPPER_CLASS = 'grid-area--content';
type Props = {
claim: StreamClaim,
costInfo: ?{ includesData: boolean, cost: number },
fileInfo: FileListItem,
uri: string,
claimIsMine: boolean,
costInfo: ?{ cost: number },
fetchFileInfo: string => void,
fetchCostInfo: string => void,
setViewed: string => void,
isSubscribed: ?string,
isSubscribed: boolean,
channelUri: string,
viewCount: number,
renderMode: string,
markSubscriptionRead: (string, string) => void,
fetchViewCount: string => void,
balance: number,
isText: boolean,
};
class FilePage extends React.Component<Props> {
componentDidMount() {
const { uri, claim, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed, fetchViewCount } = this.props;
const { uri, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed } = this.props;
if (isSubscribed) {
this.removeFromSubscriptionNotifications();
}
fetchViewCount(claim.claim_id);
// always refresh file info when entering file page to see if we have the file
// this could probably be refactored into more direct components now
// @if TARGET='app'
fetchFileInfo(uri);
// @endif
@ -46,16 +50,12 @@ class FilePage extends React.Component<Props> {
}
componentDidUpdate(prevProps: Props) {
const { isSubscribed, claim, uri, fileInfo, setViewed, fetchViewCount, fetchFileInfo } = this.props;
const { isSubscribed, uri, fileInfo, setViewed, fetchFileInfo } = this.props;
if (!prevProps.isSubscribed && isSubscribed) {
this.removeFromSubscriptionNotifications();
}
if (prevProps.uri !== uri) {
fetchViewCount(claim.claim_id);
}
if (prevProps.uri !== uri) {
setViewed(uri);
}
@ -74,26 +74,74 @@ class FilePage extends React.Component<Props> {
markSubscriptionRead(channelUri, uri);
}
render() {
const { uri, claimIsMine, costInfo, fileInfo, balance, isText } = this.props;
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
renderFilePageLayout(uri: string, mode: string, cost: ?number) {
if (RENDER_MODES.FLOATING_MODES.includes(mode)) {
return (
<React.Fragment>
<FileRenderHeader uri={uri} />
<div className={FILE_WRAPPER_CLASS}>
<FileRenderInitiator uri={uri} />
</div>
{/* playables will be rendered and injected by <FileRenderFloating> */}
<FileTitle uri={uri} />
</React.Fragment>
);
}
if (RENDER_MODES.UNRENDERABLE_MODES.includes(mode)) {
return (
<React.Fragment>
<FileRenderHeader uri={uri} />
<FileTitle uri={uri} />
<FileRenderDownload uri={uri} isFree={cost === 0} />
</React.Fragment>
);
}
if (RENDER_MODES.TEXT_MODES.includes(mode)) {
return (
<React.Fragment>
<FileRenderHeader uri={uri} />
<FileTitle uri={uri} />
<FileRenderInitiator uri={uri} />
<FileRenderInline uri={uri} />
</React.Fragment>
);
}
return (
<Page className="main--file-page">
{!fileInfo && insufficientCredits && (
<div className="media__insufficient-credits help--warning">
<I18nMessage
tokens={{
reward_link: <Button button="link" navigate="/$/rewards" label={__('Rewards')} />,
}}
>
The publisher has chosen to charge LBC to view this content. Your balance is currently too low to view it.
Check out %reward_link% for free LBC or send more LBC to your wallet.
</I18nMessage>
</div>
)}
<React.Fragment>
<FileRenderHeader uri={uri} />
<FileRenderInitiator uri={uri} />
<FileRenderInline uri={uri} />
<FileTitle uri={uri} />
</React.Fragment>
);
}
{isText ? <LayoutWrapperText uri={uri} /> : <LayoutWrapperFile uri={uri} />}
render() {
const { uri, renderMode, costInfo } = this.props;
return (
<Page className="file-page">
<div className={classnames('section card-stack', `file-page__${renderMode}`)}>
{this.renderFilePageLayout(uri, renderMode, costInfo ? costInfo.cost : null)}
</div>
<div className="section columns">
<div className="card-stack">
<FileDetails uri={uri} />
<Card
title={__('Leave a Comment')}
actions={
<div>
<CommentCreate uri={uri} />
<CommentsList uri={uri} />
</div>
}
/>
</div>
<RecommendedContent uri={uri} />
</div>
</Page>
);
}

View file

@ -126,7 +126,7 @@ class HelpPage extends React.PureComponent<Props, State> {
}
return (
<Page>
<Page className="card-stack">
<Card
title={__('Read the FAQ')}
subtitle={__('Our FAQ answers many common questions.')}
@ -202,20 +202,18 @@ class HelpPage extends React.PureComponent<Props, State> {
<WalletBackup />
{/* @endif */}
<section className="card">
<header className="table__header">
<div className="table__header-text">
<h2 className="section__title">{__('About')}</h2>
{this.state.upgradeAvailable !== null && this.state.upgradeAvailable && (
<p className="section__subtitle">
{__('A newer version of LBRY is available.')}{' '}
<Card
title={__('About')}
subtitle={
this.state.upgradeAvailable !== null && this.state.upgradeAvailable ? (
<span>
{__('A newer version of LBRY is available.')}
<Button button="link" href={newVerLink} label={__('Download now!')} />
</p>
)}
</div>
</header>
</span>
) : null
}
isBodyTable
body={
<div className="table__wrapper">
<table className="table table--stretch">
<tbody>
@ -278,7 +276,8 @@ class HelpPage extends React.PureComponent<Props, State> {
</tbody>
</table>
</div>
</section>
}
/>
</Page>
);
}

View file

@ -79,8 +79,8 @@ export default function SearchPage(props: Props) {
<div className="claim-preview__actions--header">
<ClaimUri uri={uriFromQuery} noShortUrl />
<Button
button="link"
className="media__uri--right"
button="alt"
label={__('View top claims for %normalized_uri%', {
normalized_uri: uriFromQuery,
})}

View file

@ -258,7 +258,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
const endHours = ['5', '6', '7', '8'];
return (
<Page>
<Page className="card-stack">
{!IS_WEB && noDaemonSettings ? (
<section className="card card--section">
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>

View file

@ -5,76 +5,62 @@ import {
selectClaimsByUri,
makeSelectClaimsInChannelForCurrentPageState,
makeSelectClaimIsNsfw,
makeSelectClaimIsMine,
makeSelectRecommendedContentForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
selectBalance,
selectBlockedChannels,
parseURI,
makeSelectContentTypeForUri,
makeSelectFileNameForUri,
} from 'lbry-redux';
import { selectAllCostInfoByUri } from 'lbryinc';
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import * as RENDER_MODES from 'constants/file_render_modes';
import path from 'path';
import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
// @if TARGET='web'
import { generateStreamUrl } from 'util/lbrytv';
// @endif
const RECENT_HISTORY_AMOUNT = 10;
const HISTORY_ITEMS_PER_PAGE = 50;
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 makeSelectIsPlaying = (uri: string) =>
createSelector(
selectPlayingUri,
playingUri => playingUri === uri
);
export const makeSelectIsPlaying = (uri: string) => createSelector(selectPlayingUri, playingUri => playingUri === uri);
export const makeSelectContentPositionForUri = (uri: string) =>
createSelector(
selectState,
makeSelectClaimForUri(uri),
(state, claim) => {
createSelector(selectState, makeSelectClaimForUri(uri), (state, claim) => {
if (!claim) {
return null;
}
const outpoint = `${claim.txid}:${claim.nout}`;
const id = claim.claim_id;
return state.positions[id] ? state.positions[id][outpoint] : null;
}
);
});
export const selectHistory = createSelector(
selectState,
state => state.history || []
);
export const selectHistory = createSelector(selectState, state => state.history || []);
export const selectHistoryPageCount = createSelector(
selectHistory,
history => Math.ceil(history.length / HISTORY_ITEMS_PER_PAGE)
export const selectHistoryPageCount = createSelector(selectHistory, history =>
Math.ceil(history.length / HISTORY_ITEMS_PER_PAGE)
);
export const makeSelectHistoryForPage = (page: number) =>
createSelector(
selectHistory,
selectClaimsByUri,
(history, claimsByUri) => {
createSelector(selectHistory, selectClaimsByUri, (history, claimsByUri) => {
const left = page * HISTORY_ITEMS_PER_PAGE;
const historyItemsForPage = history.slice(left, left + HISTORY_ITEMS_PER_PAGE);
return historyItemsForPage;
}
);
});
export const makeSelectHistoryForUri = (uri: string) =>
createSelector(
selectHistory,
history => history.find(i => i.uri === uri)
);
createSelector(selectHistory, history => history.find(i => i.uri === uri));
export const makeSelectHasVisitedUri = (uri: string) =>
createSelector(
makeSelectHistoryForUri(uri),
history => Boolean(history)
);
createSelector(makeSelectHistoryForUri(uri), history => Boolean(history));
export const makeSelectNextUnplayedRecommended = (uri: string) =>
createSelector(
@ -132,17 +118,12 @@ export const makeSelectNextUnplayedRecommended = (uri: string) =>
}
);
export const selectRecentHistory = createSelector(
selectHistory,
history => {
export const selectRecentHistory = createSelector(selectHistory, history => {
return history.slice(0, RECENT_HISTORY_AMOUNT);
}
);
});
export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string) =>
createSelector(
makeSelectClaimsInChannelForCurrentPageState(channel),
channelClaims => {
createSelector(makeSelectClaimsInChannelForCurrentPageState(channel), channelClaims => {
if (uris) return uris;
if (channelClaims) {
@ -151,32 +132,91 @@ export const makeSelectCategoryListUris = (uris: ?Array<string>, channel: string
}
return null;
}
);
});
export const makeSelectShouldObscurePreview = (uri: string) =>
createSelector(
selectShowMatureContent,
makeSelectClaimIsNsfw(uri),
(showMatureContent, isClaimMature) => {
createSelector(selectShowMatureContent, makeSelectClaimIsNsfw(uri), (showMatureContent, isClaimMature) => {
return isClaimMature && !showMatureContent;
});
// should probably be in lbry-redux, yarn link was fighting me
export const makeSelectFileExtensionForUri = (uri: string) =>
createSelector(makeSelectFileNameForUri(uri), fileName => {
return fileName && path.extname(fileName).substring(1);
});
let makeSelectStreamingUrlForUriWebProxy;
// @if TARGET='web'
makeSelectStreamingUrlForUriWebProxy = (uri: string) =>
createSelector(makeSelectClaimForUri(uri), claim => (claim ? generateStreamUrl(claim.name, claim.claim_id) : null));
// @endif
// @if TARGET='app'
makeSelectStreamingUrlForUriWebProxy = (uri: string) => createSelector(makeSelectStreamingUrlForUri(uri), url => url);
// @endif
export { makeSelectStreamingUrlForUriWebProxy };
export const makeSelectFileRenderModeForUri = (uri: string) =>
createSelector(
makeSelectContentTypeForUri(uri),
makeSelectMediaTypeForUri(uri),
makeSelectFileExtensionForUri(uri),
(contentType, mediaType, extension) => {
if (mediaType === 'video' || FORCE_CONTENT_TYPE_PLAYER.includes(contentType)) {
return RENDER_MODES.VIDEO;
}
if (mediaType === 'image') {
return RENDER_MODES.IMAGE;
}
if (['md', 'markdown'].includes(extension) || ['text/md', 'text/markdown'].includes(contentType)) {
return RENDER_MODES.MARKDOWN;
}
if (contentType === 'application/pdf') {
return RENDER_MODES.PDF;
}
if (['text/htm', 'text/html'].includes(contentType)) {
return RENDER_MODES.HTML;
}
if (['text', 'document', 'script'].includes(mediaType)) {
return RENDER_MODES.DOCUMENT;
}
if (extension === 'docx') {
return RENDER_MODES.DOCX;
}
// when writing this my local copy of Lbry.getMediaType had '3D-file', but I was receiving model...'
if (['3D-file', 'model'].includes(mediaType)) {
return RENDER_MODES.CAD;
}
if (mediaType === 'comic-book') {
return RENDER_MODES.COMIC;
}
if (
[
'application/zip',
'application/x-gzip',
'application/x-gtar',
'application/x-tgz',
'application/vnd.rar',
'application/x-7z-compressed',
].includes(contentType)
) {
return RENDER_MODES.DOWNLOAD;
}
if (mediaType === 'application') {
return RENDER_MODES.APPLICATION;
}
return RENDER_MODES.UNSUPPORTED;
}
);
export const makeSelectCanAutoplay = (uri: string) =>
export const makeSelectInsufficientCreditsForUri = (uri: string) =>
createSelector(
makeSelectMediaTypeForUri(uri),
mediaType => {
const canAutoPlay = ['audio', 'video', 'image', 'text', 'document'].includes(mediaType);
return canAutoPlay;
}
);
export const makeSelectIsText = (uri: string) =>
createSelector(
makeSelectMediaTypeForUri(uri),
mediaType => {
const isText = ['text', 'document', 'script'].includes(mediaType);
return isText;
makeSelectClaimIsMine(uri),
makeSelectCostInfoForUri(uri),
selectBalance,
(isMine, costInfo, balance) => {
return !isMine && costInfo && costInfo.cost > 0 && costInfo.cost > balance;
}
);

View file

@ -1,8 +1,3 @@
.button {
display: inline-block;
font-weight: var(--font-weight-base);
}
.button--uri-indicator {
@extend .button--link;
color: var(--color-text-subtitle);

View file

@ -1,6 +1,5 @@
.card {
background-color: var(--color-card-background);
margin-bottom: var(--spacing-large);
position: relative;
border-radius: var(--card-radius);
overflow: hidden;
@ -79,6 +78,12 @@
justify-content: center;
}
.card-stack {
.card:not(:last-of-type) {
margin-bottom: var(--spacing-large);
}
}
.card__list {
column-count: 2;
column-gap: var(--spacing-large);
@ -86,7 +91,7 @@
.card {
display: inline-block;
margin: 0 0 var(--spacing-large);
margin-bottom: var(--spacing-large);
}
@media (max-width: $breakpoint-small) {
@ -139,23 +144,49 @@
background-color: black;
}
.card__media--disabled {
opacity: 0.5;
pointer-events: none;
}
.card__header {
margin: var(--spacing-medium) var(--spacing-large);
display: flex;
align-items: flex-start;
.section__subtitle {
margin-bottom: 0;
.icon__wrapper {
margin-right: var(--spacing-large);
}
}
.card__title {
font-size: var(--font-title);
font-weight: var(--font-weight-light);
display: block;
/* .badge rule inherited from file page prices, should be refactored */
.badge {
float: right;
margin-left: var(--spacing-small);
margin-top: 8px; // should be flex'd, but don't blame me! I just moved it down 3px
}
}
.card__subtitle {
color: var(--color-text-subtitle);
margin: var(--spacing-small) 0;
font-size: var(--font-body);
}
.card__body {
padding: var(--spacing-large);
&:not(.card__body--no-title) {
padding-top: 0;
}
&.card__body--table {
padding: 0;
border-top: 1px solid var(--color-border);
}
@media (max-width: $breakpoint-small) {
padding: var(--spacing-large);
}
}
.card__main-actions {
padding: var(--spacing-large);
@ -177,3 +208,14 @@
margin-left: var(--spacing-small);
}
}
.card__header,
.card__body,
.card__main-actions {
@media (max-width: $breakpoint-small) {
padding: var(--spacing-small);
padding-bottom: 0;
margin: 0;
margin-bottom: var(--spacing-small);
}
}

View file

@ -9,8 +9,8 @@ $metadata-z-index: 1;
box-sizing: content-box;
color: #fff;
.button {
color: #fff;
.button--alt {
padding: 0 var(--spacing-small);
}
}

View file

@ -67,10 +67,6 @@
list-style: none;
}
.claim-preview__wrapper--channel {
background-color: var(--color-card-background-highlighted);
}
.claim-preview__wrapper--notice {
background-color: var(--color-notice);
}
@ -208,6 +204,10 @@
display: flex;
flex-direction: column;
flex: 1;
@media (max-width: $breakpoint-small) {
margin-bottom: var(--spacing-small);
}
}
.claim-preview-info {

View file

@ -1,17 +1,19 @@
.comments {
padding-top: var(--spacing-large);
}
.comment {
padding: var(--spacing-small) 0;
display: flex;
flex-direction: row;
font-size: var(--font-body);
padding: var(--spacing-medium) 0;
margin: 0;
&:first-of-type {
border-top: 1px solid var(--color-border);
}
&:not(:last-of-type) {
border-bottom: 1px solid var(--color-border);
padding: var(--spacing-medium) 0;
}
&:last-of-type {
padding-top: var(--spacing-medium);
}
}

View file

@ -89,6 +89,8 @@
position: relative;
display: flex;
align-items: center;
border-radius: var(--card-radius);
border: 1px solid var(--color-border);
justify-content: center;
background-position: 50% 50%;
background-repeat: no-repeat;
@ -110,7 +112,7 @@
}
}
.content__cover--disabled {
.content__cover--none {
@include thumbnail;
cursor: default;
position: relative;
@ -128,8 +130,21 @@
}
}
.content__cover--hidden-for-text {
display: none;
.content__cover--disabled {
pointer-events: none;
.nag {
/* boo fire Jeremy */
pointer-events: auto;
}
&:after {
content: '';
background: rgba(255, 255, 255, 0.5);
top: 0;
left: 0;
bottom: 0;
right: 0;
position: absolute;
}
}
.content__loading {

View file

@ -1,17 +1,48 @@
.file-page {
.grid-area--content + .card,
.file-render + .card,
.content__cover + .card,
.card + .file-render,
.card + .grid-area--content,
.card + .content__cover {
margin-top: var(--spacing-large);
}
.card + .file-render {
margin-top: var(--spacing-large);
}
.file-page__md {
.card {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.card + .file-render {
margin-top: 0;
.card {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
border-top: none;
}
}
}
}
.file-render {
width: 100%;
height: 100%;
z-index: 1;
overflow: hidden;
max-height: var(--inline-player-max-height);
@media (max-width: $breakpoint-small) {
// margin-top: 10px;
}
}
.file-render--document {
max-height: none;
overflow: auto;
.content__loading {
background-color: transparent;
@ -22,6 +53,21 @@
color: var(--color-text);
}
}
.markdown-preview {
height: 100%;
overflow: auto;
width: 40em;
margin-left: auto;
margin-right: auto;
max-width: unset;
min-width: unset;
@media (max-width: $breakpoint-small) {
width: 100%;
padding: var(--spacing-small);
}
}
}
.file-render__viewer {
@ -45,27 +91,16 @@
}
}
.file-render__viewer--document {
@extend .file-render__viewer;
overflow: auto;
.markdown-preview {
height: 100%;
overflow: auto;
@media (max-width: $breakpoint-small) {
padding: var(--spacing-small);
.file-render__viewer--iframe {
display: flex; /*this eliminates extra height from whitespace, if someone edits this with a better technique, tell Jeremy*/
/*
ideally iframes would dynamiclly grow, see <IframeReact> for a start at this
for now, since we don't know size, let's make as large as we can without being larger than available area
*/
iframe {
height: calc(100vh - var(--header-height) - var(--spacing-medium) * 2);
}
}
}
.file-render__viewer--pdf {
@extend .file-render__viewer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
}
.file-render__content {
width: 100%;
@ -138,6 +173,8 @@
}
.file-render {
border-radius: var(--card-radius);
.video-js {
display: flex;
align-items: center;

View file

@ -33,33 +33,6 @@
}
}
.main--file-page {
position: relative;
.grid-area--content {
max-height: var(--inline-player-max-height);
}
.grid-area--info {
margin-right: var(--spacing-large);
width: 52.5%;
}
.grid-area--related {
width: calc(47.5% - var(--spacing-large));
}
@media (max-width: $breakpoint-small) {
overflow-x: hidden;
.grid-area--related,
.grid-area--info {
margin-right: 0;
width: 100%;
}
}
}
.main--auth-page {
max-width: 60rem;
margin-top: var(--spacing-main-padding);
@ -75,6 +48,9 @@
margin-top: 100px;
margin-bottom: 100px;
text-align: center;
> .card {
width: 100%;
}
}
.main--launching {
@ -100,26 +76,3 @@
.main--full-width {
width: 100%;
}
.main__document-wrapper {
max-width: 100%;
min-width: 40em;
width: fit-content;
margin: auto;
margin-bottom: var(--spacing-xlarge);
@media (max-width: $breakpoint-small) {
width: 100%;
}
}
.main__document-wrapper--markdown {
@extend .main__document-wrapper;
width: 40em;
max-width: unset;
min-width: unset;
@media (max-width: $breakpoint-small) {
width: 100%;
}
}

View file

@ -1,4 +1,8 @@
.markdown-preview {
> :first-child {
margin-top: 0;
}
> *:last-child {
margin-bottom: 0;
}

View file

@ -15,25 +15,6 @@
// M E D I A
// T I T L E
.media__title {
margin-bottom: var(--spacing-medium);
}
.media__title-text {
overflow: hidden;
text-overflow: ellipsis;
font-weight: var(--font-weight-bold);
white-space: normal;
font-size: var(--font-title);
display: inline;
}
.media__title-badge {
float: right;
margin-left: var(--spacing-small);
margin-top: 5px;
}
.media__uri {
position: absolute;
transform: translateY(-130%);
@ -65,14 +46,6 @@
right: 0;
}
.media__uri--large {
margin-bottom: var(--spacing-medium);
}
.media__insufficient-credits {
padding: 10px;
}
// M E D I A
// S U B T I T L E
@ -90,19 +63,8 @@
@extend .media__subtitle;
display: flex;
justify-content: space-between;
}
.media__subtitle--large {
display: block;
> button {
display: block;
}
}
.media__subtitle__channel {
font-weight: var(--font-weight-bold);
margin: var(--spacing-small) 0;
align-items: center;
margin-bottom: var(--spacing-small);
}
.media__info-text {
@ -118,13 +80,18 @@
}
.media__actions {
@extend .section__actions;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 0;
}
.media__document-thumbnail {
margin-top: 0;
@media (max-width: $breakpoint-small) {
justify-content: flex-start;
padding-top: var(--spacing-small);
> * {
margin-right: var(--spacing-small);
margin-bottom: var(--spacing-small);
}
}
}

View file

@ -4,7 +4,6 @@
justify-content: center;
vertical-align: middle;
text-align: left;
margin-bottom: var(--spacing-xlarge);
}
.yrbl {

View file

@ -13,7 +13,6 @@ $nag-error-z-index: 100001;
color: var(--color-white);
font-weight: var(--font-weight-bold);
text-align: center;
z-index: 2;
.button--link {
font-weight: var(--font-weight-bold);
@ -26,6 +25,13 @@ $nag-error-z-index: 100001;
}
}
.nag--inline {
position: absolute;
top: 0;
bottom: auto;
z-index: 1 !important; /* booooooo */
}
.nag--helpful {
background-color: var(--color-secondary);
color: var(--color-white);

View file

@ -22,21 +22,11 @@
.section__flex {
display: flex;
align-items: flex-start;
& > :first-child {
> .icon__wrapper:first-child {
margin-right: var(--spacing-large);
}
}
.section__flex-wrap {
@extend .section__flex;
flex-wrap: wrap;
margin-bottom: var(--spacing-large);
& > * {
margin-bottom: var(--spacing-large);
}
}
.section__title {
text-align: left;
font-size: var(--font-title);
@ -101,6 +91,10 @@
}
}
.section__actions--no-margin {
margin-top: 0;
}
@media (max-width: $breakpoint-small) {
.section__actions {
flex-wrap: wrap;

View file

@ -146,6 +146,7 @@ img {
& > * {
margin: 0;
margin-bottom: var(--spacing-medium);
width: 100%;
flex-basis: auto;
@ -216,12 +217,16 @@ img {
.help--inline {
@extend .help;
margin-top: 0;
margin-bottom: 0;
}
.empty {
color: var(--color-text-empty);
font-style: italic;
}
.empty--centered {
text-align: center;
}
.qr-code {
width: 134px;

View file

@ -19,9 +19,6 @@ $breakpoint-medium: 1150px;
--spacing-large: 2rem;
--spacing-xlarge: 3rem;
--spacing-main-padding: var(--spacing-xlarge);
--file-page-max-width: 1787px;
--file-max-height: 788px;
--file-max-width: 1400px;
--floating-viewer-width: 32rem;
--floating-viewer-height: 18rem; // 32 * 9/16
--floating-viewer-info-height: 5rem;

View file

@ -21,6 +21,9 @@
--color-button-secondary-bg: #395877;
--color-button-secondary-bg-hover: #4b6d8f;
--color-button-secondary-text: #a3c1e0;
--color-button-alt-bg: #4d5660;
--color-button-alt-bg-hover: #3e464d;
--color-button-alt-text: #e2e9f0;
--color-header-button: var(--color-link-icon);
// Color

View file

@ -819,9 +819,10 @@
prop-types "^15.6.2"
scheduler "^0.18.0"
"@lbry/components@^3.0.12":
version "3.0.12"
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-3.0.12.tgz#9ba4598edf26496060a97023ca0132d39c388c3b"
"@lbry/components@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.0.1.tgz#8dcf7348920383d854c0db640faaf1ac5a72f7ef"
integrity sha512-vY84ziZ9EaXoezDBK2VsajvXcSPXDV0fr1VWn2w0iHkGa756RWvNySpnqaKMZH+myK12mvNNc/NkGIW5oO7+5w==
"@mapbox/hast-util-table-cell-style@^0.1.3":
version "0.1.3"