new text viewer layout

This commit is contained in:
Sean Yesmunt 2020-01-06 13:32:35 -05:00
parent 9a6f2a1975
commit 72b9f3efdd
47 changed files with 658 additions and 274 deletions

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 FileViewer from 'component/fileViewer';
import FloatingViewer from 'component/floatingViewer';
import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous';
import Nag from 'component/common/nag';
@ -203,7 +203,7 @@ function App(props: Props) {
>
<Router />
<ModalRouter />
<FileViewer pageUri={uri} />
<FloatingViewer pageUri={uri} />
{/* @if TARGET='web' */}
<YoutubeWelcome />

View file

@ -147,6 +147,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
}}
className={combinedClassName}
activeClassName={activeClass}
{...otherProps}
>
{content}
</NavLink>

View file

@ -101,7 +101,7 @@ export default function ClaimList(props: Props) {
{header !== false && (
<React.Fragment>
{headerLabel && <label className="claim-list__header-label">{headerLabel}</label>}
<div className={classnames('claim-list__header', { 'claim-list__header--small': type === 'small' })}>
<div className={classnames('claim-list__header', { 'section__title--small': type === 'small' })}>
{header}
{loading && <Spinner type="small" />}
<div className="claim-list__alt-controls">

View file

@ -7,7 +7,7 @@ import { withRouter } from 'react-router-dom';
import { openCopyLinkMenu } from 'util/context-menu';
import { formatLbryUrlForWeb } from 'util/url';
import { isEmpty } from 'util/object';
import CardMedia from 'component/cardMedia';
import FileThumbnail from 'component/fileThumbnail';
import UriIndicator from 'component/uriIndicator';
import TruncatedText from 'component/common/truncated-text';
import DateTime from 'component/dateTime';
@ -200,7 +200,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<ChannelThumbnail uri={uri} obscure={channelIsBlocked} />
</UriIndicator>
) : (
<CardMedia thumbnail={thumbnail} />
<FileThumbnail thumbnail={thumbnail} />
)}
<div className="claim-preview__text">
<div className="claim-preview-metadata">

View file

@ -1,21 +1,33 @@
import { connect } from 'react-redux';
import { makeSelectFileInfoForUri, makeSelectClaimIsMine } from 'lbry-redux';
import * as SETTINGS from 'constants/settings';
import {
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
doPrepareEdit,
} from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doOpenModal } from 'redux/actions/app';
import FileActions from './view';
import fs from 'fs';
import FilePage from './view';
const select = (state, props) => ({
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
/* availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix */
costInfo: makeSelectCostInfoForUri(props.uri)(state),
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),
});
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
)(FileActions);
)(FilePage);

View file

@ -3,40 +3,114 @@ import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import React from 'react';
import Button from 'component/button';
import Tooltip from 'component/common/tooltip';
import FileDownloadLink from 'component/fileDownloadLink';
import { buildURI } from 'lbry-redux';
type Props = {
uri: string,
claimId: string,
openModal: (id: string, { uri: string }) => void,
claim: StreamClaim,
openModal: (id: string, { uri: string, claimIsMine?: boolean, isSupport?: boolean }) => void,
prepareEdit: ({}, string, {}) => void,
claimIsMine: boolean,
fileInfo: FileListItem,
costInfo: ?{ cost: number },
contentType: string,
supportOption: boolean,
};
class FileActions extends React.PureComponent<Props> {
render() {
const { fileInfo, uri, openModal, claimIsMine, claimId } = this.props;
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 showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
const claimId = claim && claim.claim_id;
const { signing_channel: signingChannel } = claim;
const channelName = signingChannel && signingChannel.name;
// We want to use the short form uri for editing
// This is what the user is used to seeing, they don't care about the claim id
// We will select the claim id before they publish
let editUri;
if (claimIsMine) {
const uriObject: { streamName: string, streamClaimId: string, channelName?: string } = {
streamName: claim.name,
streamClaimId: claim.claim_id,
};
if (channelName) {
uriObject.channelName = channelName;
}
editUri = buildURI(uriObject);
}
return (
<React.Fragment>
{showDelete && (
<Tooltip label={__('Remove from your library')}>
<div className="media__actions">
<div className="section__actions">
<Button
button="alt"
icon={ICONS.SHARE}
label={__('Share')}
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
/>
{!claimIsMine && (
<Button
button="alt"
icon={ICONS.TIP}
label={__('Tip')}
requiresAuth={IS_WEB}
title={__('Send a tip to this creator')}
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: false })}
/>
)}
{(claimIsMine || (!claimIsMine && supportOption)) && (
<Button
button="alt"
icon={ICONS.SUPPORT}
label={__('Support')}
requiresAuth={IS_WEB}
title={__('Support this claim')}
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: true })}
/>
)}
</div>
<div className="section__actions">
{/* @if TARGET='app' */}
<FileDownloadLink uri={uri} />
{/* @endif */}
{claimIsMine && (
<Button
button="alt"
icon={ICONS.EDIT}
label={__('Edit')}
navigate="/$/publish"
onClick={() => {
prepareEdit(claim, editUri, fileInfo);
}}
/>
)}
{showDelete && (
<Button
title={__('Remove from your library')}
button="alt"
icon={ICONS.DELETE}
description={__('Delete')}
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
/>
</Tooltip>
)}
{!claimIsMine && (
<Tooltip label={__('Report content')}>
<Button button="alt" icon={ICONS.REPORT} href={`https://lbry.com/dmca/${claimId}`} />
</Tooltip>
<Button
title={__('Report content')}
button="alt"
icon={ICONS.REPORT}
href={`https://lbry.com/dmca/${claimId}`}
/>
)}
</React.Fragment>
</div>
</div>
);
}
}
export default FileActions;

View file

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

View file

@ -0,0 +1,19 @@
// @flow
import * as React from 'react';
import ClaimPreview from 'component/claimPreview';
type Props = {
channelUri: string,
};
function LayoutWrapperDocument(props: Props) {
const { channelUri } = props;
return channelUri ? (
<ClaimPreview uri={channelUri} type="inline" properties={false} hideBlock />
) : (
<div className="claim-preview--inline claim-preview-title">{__('Anonymous')}</div>
);
}
export default LayoutWrapperDocument;

View file

@ -43,11 +43,9 @@ class FileDetails extends PureComponent<Props> {
<Fragment>
<Expandable>
{description && (
<Fragment>
<div className="media__info-text">
<MarkdownPreview content={description} />
</div>
</Fragment>
)}
<ClaimTags uri={uri} type="large" />
<table className="table table--condensed table--fixed table--file-details">

View file

@ -1,6 +1,7 @@
// @flow
import { remote } from 'electron';
import React, { Suspense, Fragment } from 'react';
import classnames from 'classnames';
import LoadingScreen from 'component/common/loading-screen';
import VideoViewer from 'component/viewers/videoViewer';
import ImageViewer from 'component/viewers/imageViewer';
@ -186,8 +187,10 @@ class FileRender extends React.PureComponent<Props> {
}
render() {
const { mediaType } = this.props;
return (
<div className="file-render">
<div className={classnames('file-render', { 'file-render--document': mediaType === 'text' })}>
<Suspense fallback={<div />}>{this.renderViewer()}</Suspense>
</div>
);

View file

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { makeSelectViewCountForUri } from 'lbryinc';
import FileViewCount from './view';
const select = (state, props) => ({
viewCount: makeSelectViewCountForUri(props.uri)(state),
});
export default connect(select)(FileViewCount);

View file

@ -0,0 +1,20 @@
// @flow
import React from 'react';
import HelpLink from 'component/common/help-link';
type Props = {
viewCount: string,
};
function LayoutWrapperDocument(props: Props) {
const { viewCount } = props;
return (
<span>
{viewCount !== 1 ? __('%view_count% Views', { view_count: viewCount }) : __('1 View')}
<HelpLink href="https://lbry.com/faq/views" />
</span>
);
}
export default LayoutWrapperDocument;

View file

@ -1,6 +1,6 @@
// @flow
// This component is entirely for triggering the start of a file view
// The actual viewer for a file exists in FileViewer
// The actual viewer for a file exists in TextViewer and FloatingViewer
// They can't exist in one component because we need to handle/listen for the start of a new file view
// while a file is currently being viewed
import React, { useEffect, useCallback, Fragment } from 'react';
@ -27,9 +27,10 @@ type Props = {
hasCostInfo: boolean,
costInfo: any,
isAutoPlayable: boolean,
inline: boolean,
};
export default function FileViewer(props: Props) {
export default function FileViewerInitiator(props: Props) {
const {
play,
mediaType,
@ -49,6 +50,7 @@ export default function FileViewer(props: Props) {
const cost = costInfo && costInfo.cost;
const forceVideo = ['application/x-ext-mkv', 'video/x-matroska'].includes(contentType);
const isPlayable = ['audio', 'video'].includes(mediaType) || forceVideo;
const isText = mediaType === 'text';
const fileStatus = fileInfo && fileInfo.status;
const webStreamOnly = contentType === 'application/pdf' || mediaType === 'text';
const supported = IS_WEB ? (!cost && isStreamable) || webStreamOnly || forceVideo : true;
@ -95,10 +97,10 @@ export default function FileViewer(props: Props) {
useEffect(() => {
const videoOnPage = document.querySelector('video');
if (autoplay && !videoOnPage && isAutoPlayable && hasCostInfo && cost === 0) {
if (((autoplay && !videoOnPage && isAutoPlayable) || isText) && hasCostInfo && cost === 0) {
viewFile();
}
}, [autoplay, viewFile, isAutoPlayable, hasCostInfo, cost]);
}, [autoplay, viewFile, isAutoPlayable, hasCostInfo, cost, isText]);
return (
<div
@ -108,6 +110,7 @@ export default function FileViewer(props: Props) {
className={classnames({
content__cover: supported,
'content__cover--disabled': !supported,
'content__cover--hidden-for-text': isText,
'card__media--nsfw': obscurePreview,
'card__media--disabled': supported && !fileInfo && insufficientCredits,
})}
@ -127,6 +130,7 @@ export default function FileViewer(props: Props) {
}
/>
)}
{!isPlaying && supported && (
<Button
onClick={viewFile}

View file

@ -8,7 +8,7 @@ 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 'page/file/view';
import { FILE_WRAPPER_CLASS } from 'component/layoutWrapperFile/view';
import Draggable from 'react-draggable';
import Tooltip from 'component/common/tooltip';
import { onFullscreenChange } from 'util/full-screen';
@ -96,7 +96,6 @@ export default function FileViewer(props: Props) {
function handleResize() {
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
if (!element) {
console.error("Can't find file viewer wrapper to attach to the inline viewer to"); // eslint-disable-line
return;
}
@ -125,7 +124,10 @@ export default function FileViewer(props: Props) {
}
const hidePlayer =
!isPlaying || !uri || (!inline && (isMobile || !floatingPlayerEnabled || !['audio', 'video'].includes(mediaType)));
mediaType === 'text' ||
!isPlaying ||
!uri ||
(!inline && (isMobile || !floatingPlayerEnabled || !['audio', 'video'].includes(mediaType)));
if (hidePlayer) {
return null;

View file

@ -0,0 +1,35 @@
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

@ -0,0 +1,89 @@
// @flow
import * as React from 'react';
import { normalizeURI } from 'lbry-redux';
import FileViewerInitiator from 'component/fileViewerInitiator';
import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails';
import FileAuthor from 'component/fileAuthor';
import FileActions from 'component/fileActions';
import DateTime from 'component/dateTime';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
import ClaimUri from 'component/claimUri';
import FileViewCount from 'component/fileViewCount';
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">
<div className="media__subtitle--between">
<DateTime uri={uri} show={DateTime.SHOW_DATE} />
<FileViewCount uri={uri} />
</div>
<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

@ -0,0 +1,39 @@
import { connect } from 'react-redux';
import * as SETTINGS from 'constants/settings';
import {
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
doPrepareEdit,
makeSelectTitleForUri,
makeSelectMetadataForUri,
makeSelectThumbnailForUri,
} 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),
metadata: makeSelectMetadataForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(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

@ -0,0 +1,98 @@
// @flow
import * as React from 'react';
import { normalizeURI } from 'lbry-redux';
import FilePrice from 'component/filePrice';
import FileAuthor from 'component/fileAuthor';
import FileThumbnail from 'component/fileThumbnail';
import FileViewCount from 'component/fileViewCount';
import FileActions from 'component/fileActions';
import TextViewer from 'component/textViewer';
import DateTime from 'component/dateTime';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
import MarkdownPreview from 'component/common/markdown-preview';
import ClaimUri from 'component/claimUri';
import FileViewerInitiator from 'component/fileViewerInitiator';
type Props = {
uri: string,
metadata: StreamMetadata,
title: string,
nsfw: boolean,
claim: StreamClaim,
thumbnail: ?string,
};
function LayoutWrapperDocument(props: Props) {
const { uri, claim, metadata, title, nsfw, thumbnail } = props;
const { description } = metadata;
return (
<div className="">
<div className="main__document-wrapper">
<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>
</div>
<div className="media__document-thumbnail">
<FileThumbnail thumbnail={thumbnail} />
</div>
<div className="section main__document-wrapper">
<div className="section__subtitle">
<em>
<MarkdownPreview content={description} />
</em>
</div>
<div className="media__subtitle--between">
<DateTime uri={uri} show={DateTime.SHOW_DATE} />
<FileViewCount uri={uri} />
</div>
<FileActions uri={uri} />
<div className="section__divider">
<hr />
</div>
<FileAuthor uri={uri} />
<div className="section__divider">
<hr />
</div>
{/* Render the initiator to trigger the view of the file */}
<FileViewerInitiator uri={uri} />
<TextViewer uri={uri} />
<div className="section__divider">
<hr />
</div>
</div>
<div className="columns">
<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 LayoutWrapperDocument;

View file

@ -0,0 +1,34 @@
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

@ -0,0 +1,60 @@
// @flow
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import FileRender from 'component/fileRender';
import usePrevious from 'effects/use-previous';
type Props = {
mediaType: string,
contentType: string,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
isStreamable: boolean,
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;
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);
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);
setPlayTime(null);
});
}
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, hasRecordedView, playTime, uri, claimRewards]);
return (
<div className={classnames('content__viewersss')}>
{isReadyToPlay ? <FileRender uri={uri} /> : <div className="placeholder--text-document" />}
</div>
);
}

View file

@ -98,12 +98,11 @@ class DocumentViewer extends React.PureComponent<Props, State> {
render() {
const { error, loading, content } = this.state;
const isReady = content && !error;
const loadingMessage = __('Rendering document.');
const errorMessage = __("Sorry, looks like we can't load the document.");
return (
<div className="file-render__viewer--document">
{loading && !error && <LoadingScreen status={loadingMessage} spinner />}
{loading && !error && <div className="placeholder--text-document" />}
{error && <LoadingScreen status={errorMessage} spinner={!error} />}
{isReady && this.renderDocument()}
</div>

View file

@ -1,7 +1,7 @@
// @flow
import React from 'react';
import Button from 'component/button';
import CardMedia from 'component/cardMedia';
import FileThumbnail from 'component/fileThumbnail';
type Props = {
params: UpdatePublishFormData,
progress: string,
@ -13,7 +13,7 @@ export default function WebUploadItem(props: Props) {
return (
<li className={'claim-preview claim-preview--inactive card--inline'}>
<CardMedia thumbnail={params.thumbnail_url} />
<FileThumbnail thumbnail={params.thumbnail_url} />
<div className={'claim-preview-metadata'}>
<div className="claim-preview-info">
<div className="claim-preview-title">{params.title}</div>

View file

@ -1,6 +1,6 @@
import React from 'react';
import WalletAddress from './node_modules/component/walletAddress';
import Page from './node_modules/component/page';
import WalletAddress from 'component/walletAddress';
import Page from 'component/page';
const WalletAddressPage = () => (
<Page className="main--contained">

View file

@ -1,5 +1,5 @@
import React from 'react';
import WalletSend from './node_modules/component/walletSend';
import WalletSend from 'component/walletSend';
const WalletSendModal = () => (
<div>

View file

@ -1,32 +1,23 @@
import { connect } from 'react-redux';
import * as settings from 'constants/settings';
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
import { doSetClientSetting } from 'redux/actions/settings';
import { doSetContentHistoryItem } from 'redux/actions/content';
import {
doFetchFileInfo,
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
makeSelectMetadataForUri,
makeSelectChannelForClaimUri,
selectBalance,
makeSelectTitleForUri,
makeSelectThumbnailForUri,
makeSelectClaimIsNsfw,
doPrepareEdit,
makeSelectMediaTypeForUri,
} from 'lbry-redux';
import { doFetchViewCount, makeSelectViewCountForUri, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
import { doFetchViewCount, makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { doOpenModal } from 'redux/actions/app';
import fs from 'fs';
import FilePage from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowMatureContent(state),
@ -34,20 +25,13 @@ const select = (state, props) => ({
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
viewCount: makeSelectViewCountForUri(props.uri)(state),
balance: selectBalance(state),
title: makeSelectTitleForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
supportOption: makeSelectClientSetting(settings.SUPPORT_OPTION)(state),
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
});
const perform = dispatch => ({
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
fetchViewCount: claimId => dispatch(doFetchViewCount(claimId)),

View file

@ -1,31 +1,14 @@
// @flow
import * as MODALS from 'constants/modal_types';
import * as icons from 'constants/icons';
import * as React from 'react';
import { buildURI, normalizeURI } from 'lbry-redux';
import FileViewerInitiator from 'component/fileViewerInitiator';
import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails';
import FileActions from 'component/fileActions';
import DateTime from 'component/dateTime';
import Button from 'component/button';
import Page from 'component/page';
import FileDownloadLink from 'component/fileDownloadLink';
import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate';
import ClaimUri from 'component/claimUri';
import ClaimPreview from 'component/claimPreview';
import HelpLink from 'component/common/help-link';
import I18nMessage from 'component/i18nMessage/view';
export const FILE_WRAPPER_CLASS = 'grid-area--content';
import LayoutWrapperFile from 'component/layoutWrapperFile';
import LayoutWrapperText from 'component/layoutWrapperText';
type Props = {
claim: StreamClaim,
fileInfo: FileListItem,
contentType: string,
uri: string,
claimIsMine: boolean,
costInfo: ?{ cost: number },
@ -36,14 +19,10 @@ type Props = {
isSubscribed: boolean,
channelUri: string,
viewCount: number,
prepareEdit: ({}, string, {}) => void,
openModal: (id: string, { uri: string, claimIsMine?: boolean, isSupport?: boolean }) => void,
markSubscriptionRead: (string, string) => void,
fetchViewCount: string => void,
balance: number,
title: string,
nsfw: boolean,
supportOption: boolean,
mediaType: string,
};
class FilePage extends React.Component<Props> {
@ -96,50 +75,11 @@ class FilePage extends React.Component<Props> {
}
render() {
const {
claim,
contentType,
uri,
openModal,
claimIsMine,
prepareEdit,
costInfo,
fileInfo,
channelUri,
viewCount,
balance,
title,
nsfw,
supportOption,
} = this.props;
// File info
const { signing_channel: signingChannel } = claim;
const channelName = signingChannel && signingChannel.name;
const webShareable =
costInfo && costInfo.cost === 0 && contentType && ['video', 'image', 'audio'].includes(contentType.split('/')[0]);
// We want to use the short form uri for editing
// This is what the user is used to seeing, they don't care about the claim id
// We will select the claim id before they publish
let editUri;
if (claimIsMine) {
const uriObject: { streamName: string, streamClaimId: string, channelName?: string } = {
streamName: claim.name,
streamClaimId: claim.claim_id,
};
if (channelName) {
uriObject.channelName = channelName;
}
editUri = buildURI(uriObject);
}
const { uri, claimIsMine, costInfo, fileInfo, balance, mediaType } = this.props;
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
return (
<Page className="main--file-page">
<ClaimUri uri={uri} />
<div className={`card ${FILE_WRAPPER_CLASS}`}>
{!fileInfo && insufficientCredits && (
<div className="media__insufficient-credits help--warning">
<I18nMessage
@ -147,109 +87,13 @@ class FilePage extends React.Component<Props> {
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.
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>
)}
<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">
<div className="media__subtitle--between">
<DateTime uri={uri} show={DateTime.SHOW_DATE} />
<span>
{viewCount !== 1 ? __('%view_count% Views', { view_count: viewCount }) : __('1 View')}
<HelpLink href="https://lbry.com/faq/views" />
</span>
</div>
<div className="media__actions">
<div className="section__actions">
{claimIsMine && (
<Button
button="alt"
icon={icons.EDIT}
label={__('Edit')}
navigate="/$/publish"
onClick={() => {
prepareEdit(claim, editUri, fileInfo);
}}
/>
)}
<Button
button="alt"
icon={icons.SHARE}
label={__('Share')}
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
/>
{!claimIsMine && (
<Button
button="alt"
icon={icons.TIP}
label={__('Tip')}
requiresAuth={IS_WEB}
title={__('Send a tip to this creator')}
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: false })}
/>
)}
{(claimIsMine || (!claimIsMine && supportOption)) && (
<Button
button="alt"
icon={icons.SUPPORT}
label={__('Support')}
requiresAuth={IS_WEB}
title={__('Support this claim')}
onClick={() => openModal(MODALS.SEND_TIP, { uri, claimIsMine, isSupport: true })}
/>
)}
</div>
<div className="section__actions">
{/* @if TARGET='app' */}
<FileDownloadLink uri={uri} />
{/* @endif */}
<FileActions uri={uri} claimId={claim.claim_id} />
</div>
</div>
<div className="section__divider">
<hr />
</div>
{channelUri ? (
<ClaimPreview uri={channelUri} type="inline" properties={false} hideBlock />
) : (
<div className="claim-preview--inline claim-preview-title">{__('Anonymous')}</div>
)}
<FileDetails uri={uri} />
<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>
{mediaType === 'text' ? <LayoutWrapperText uri={uri} /> : <LayoutWrapperFile uri={uri} />}
</Page>
);
}

View file

@ -17,10 +17,6 @@
}
}
.claim-list__header--small {
color: var(--color-text-subtitle);
}
.claim-list__dropdown {
padding: 0 var(--spacing-medium);
@ -139,7 +135,6 @@
.claim-preview--inline {
padding: 0;
border-bottom: none;
margin-bottom: var(--spacing-medium);
.channel-thumbnail {
width: var(--channel-thumbnail-width--small);

View file

@ -102,6 +102,10 @@
}
}
.content__cover--hidden-for-text {
display: none;
}
.content__loading {
height: 100%;
display: flex;

View file

@ -6,6 +6,10 @@
max-height: var(--inline-player-max-height);
}
.file-render--document {
max-height: none;
}
.file-render__viewer {
width: 100%;
height: 100%;
@ -26,12 +30,14 @@
.file-render__viewer--document {
@extend .file-render__viewer;
overflow: auto;
background-color: var(--color-file-viewer-background);
.markdown-preview {
height: 100%;
overflow: auto;
padding: var(--spacing-large);
@media (max-width: $breakpoint-small) {
padding: var(--spacing-small);
}
}
}

View file

@ -119,10 +119,6 @@
border-radius: 1.5rem;
margin-left: var(--spacing-small);
svg {
stroke: var(--color-text);
}
&:hover {
background-color: var(--color-primary-alt);
}

View file

@ -25,7 +25,6 @@
.main {
position: relative;
width: calc(100% - var(--side-nav-width) - var(--spacing-large));
margin-right: var(--spacing-main-padding);
@media (max-width: $breakpoint-small) {
width: 100%;
@ -101,3 +100,12 @@
.main--full-width {
width: 100%;
}
.main__document-wrapper {
width: 60%;
margin: auto;
@media (max-width: $breakpoint-small) {
width: 100%;
}
}

View file

@ -13,7 +13,33 @@
font-size: inherit;
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-medium);
padding-top: var(--spacing-medium);
&:not(:first-child) {
margin-top: var(--spacing-large);
}
}
h1 {
font-size: 1.8em;
}
h2 {
font-size: 1.7em;
}
h3 {
font-size: 1.6em;
}
h4 {
font-size: 1.5em;
}
h5 {
font-size: 1.4em;
}
h6 {
font-size: 1.3em;
}
@media (max-width: $breakpoint-small) {
font-size: 0.8em;
}
// Paragraphs
@ -28,10 +54,6 @@
}
}
// Strikethrough text
del {
}
// Tables
table {
margin-bottom: 1.2rem;
@ -54,6 +76,13 @@
img {
margin-bottom: var(--spacing-medium);
padding-top: var(--spacing-medium);
max-height: 40vh;
object-position: left;
@media (max-width: $breakpoint-small) {
max-height: 30vh;
font-size: 0.8em;
}
}
// Horizontal Rule

View file

@ -108,3 +108,7 @@
justify-content: space-between;
margin-top: 0;
}
.media__document-thumbnail {
margin-top: 0;
}

View file

@ -1,6 +1,7 @@
.navigation {
width: var(--side-nav-width);
font-size: var(--font-body);
margin-left: var(--spacing-main-padding);
@media (max-width: $breakpoint-small) {
display: none;

View file

@ -27,3 +27,8 @@
}
}
}
.placeholder--text-document {
@include placeholder;
height: 60vh;
}

View file

@ -18,6 +18,11 @@ html {
color: var(--color-text);
background-color: var(--color-background);
font-size: 16px;
}
body {
font-size: 1em;
}
h1,
@ -30,8 +35,6 @@ h6 {
}
p {
font-size: var(--font-body);
& + p {
margin-top: var(--spacing-small);
}
@ -43,7 +46,7 @@ ol {
li {
list-style-position: outside;
margin: var(--spacing-medium);
margin: var(--spacing-xsmall) var(--spacing-medium);
margin-bottom: 0;
}
}

View file

@ -12,8 +12,8 @@
--color-link: var(--color-primary);
--color-link-hover: #60e1ba;
--color-link-active: #60e1ba;
--color-link-icon: #89939e;
--color-navigation-link: var(--color-link-icon);
--color-link-icon: #6a7580;
--color-navigation-link: #b3bcc6;
--color-button-primary-bg: var(--color-primary-alt);
--color-button-primary-bg-hover: #44796c;
--color-button-primary-text: var(--color-primary);