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;
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
return (
<React.Fragment>
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 (
<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 && (
<Tooltip label={__('Remove from your library')}>
<Button
button="alt"
icon={ICONS.DELETE}
description={__('Delete')}
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
/>
</Tooltip>
<Button
title={__('Remove from your library')}
button="alt"
icon={ICONS.DELETE}
description={__('Delete')}
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
/>
)}
{!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>
<div className="media__info-text">
<MarkdownPreview content={description} />
</div>
)}
<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

@ -21,11 +21,11 @@ class CardMedia extends React.PureComponent<Props> {
// @if TARGET='web'
// Pass image urls through a compression proxy
url = thumbnail || Placeholder;
// url = thumbnail
// ? 'https://ext.thumbnails.lbry.com/400x,q55/' +
// The image server will redirect if we don't remove the double slashes after http(s)
// thumbnail.replace('https://', 'https:/').replace('http://', 'http:/')
// : Placeholder;
// url = thumbnail
// ? 'https://ext.thumbnails.lbry.com/400x,q55/' +
// The image server will redirect if we don't remove the double slashes after http(s)
// thumbnail.replace('https://', 'https:/').replace('http://', 'http:/')
// : Placeholder;
// @endif
// @if TARGET='app'
url = thumbnail || Placeholder;

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,160 +75,25 @@ 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
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>
)}
<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>
{!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>
<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);