add content placeholder and time info on file{card,tile}

This commit is contained in:
Sean Yesmunt 2018-10-10 01:22:06 -04:00
parent df65db0dfc
commit 3c87c47632
14 changed files with 181 additions and 79 deletions

View file

@ -1,6 +1,15 @@
// @flow
import React from 'react'; import React from 'react';
import moment from 'moment';
class DateTime extends React.PureComponent { type Props = {
date?: number,
timeAgo?: boolean,
formatOptions: {},
show?: string,
};
class DateTime extends React.PureComponent<Props> {
static SHOW_DATE = 'date'; static SHOW_DATE = 'date';
static SHOW_TIME = 'time'; static SHOW_TIME = 'time';
static SHOW_BOTH = 'both'; static SHOW_BOTH = 'both';
@ -29,18 +38,22 @@ class DateTime extends React.PureComponent {
} }
render() { render() {
const { date, formatOptions } = this.props; const { date, formatOptions, timeAgo } = this.props;
const show = this.props.show || DateTime.SHOW_BOTH; const show = this.props.show || DateTime.SHOW_BOTH;
const locale = app.i18n.getLocale(); const locale = app.i18n.getLocale();
if (timeAgo) {
return date ? <span>{moment(date).from(moment())}</span> : <span />;
}
return ( return (
<span> <span>
{date && {date &&
(show == DateTime.SHOW_BOTH || show === DateTime.SHOW_DATE) && (show === DateTime.SHOW_BOTH || show === DateTime.SHOW_DATE) &&
date.toLocaleDateString([locale, 'en-US'], formatOptions)} date.toLocaleDateString([locale, 'en-US'], formatOptions)}
{show == DateTime.SHOW_BOTH && ' '} {show === DateTime.SHOW_BOTH && ' '}
{date && {date &&
(show == DateTime.SHOW_BOTH || show === DateTime.SHOW_TIME) && (show === DateTime.SHOW_BOTH || show === DateTime.SHOW_TIME) &&
date.toLocaleTimeString()} date.toLocaleTimeString()}
{!date && '...'} {!date && '...'}
</span> </span>

View file

@ -14,6 +14,8 @@ import {
} from 'redux/selectors/content'; } from 'redux/selectors/content';
import { selectShowNsfw } from 'redux/selectors/settings'; import { selectShowNsfw } from 'redux/selectors/settings';
import { selectPendingPublish } from 'redux/selectors/publish'; import { selectPendingPublish } from 'redux/selectors/publish';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { doClearContentHistoryUri } from 'redux/actions/content';
import FileCard from './view'; import FileCard from './view';
const select = (state, props) => { const select = (state, props) => {
@ -36,6 +38,7 @@ const select = (state, props) => {
...fileCardInfo, ...fileCardInfo,
pending: !!pendingPublish, pending: !!pendingPublish,
position: makeSelectContentPositionForUri(props.uri)(state), position: makeSelectContentPositionForUri(props.uri)(state),
subscribed: makeSelectIsSubscribed(props.uri)(state),
}; };
}; };

View file

@ -10,6 +10,7 @@ import * as icons from 'constants/icons';
import classnames from 'classnames'; import classnames from 'classnames';
import FilePrice from 'component/filePrice'; import FilePrice from 'component/filePrice';
import { openCopyLinkMenu } from 'util/contextMenu'; import { openCopyLinkMenu } from 'util/contextMenu';
import DateTime from 'component/dateTime';
type Props = { type Props = {
uri: string, uri: string,
@ -24,15 +25,11 @@ type Props = {
/* eslint-disable react/no-unused-prop-types */ /* eslint-disable react/no-unused-prop-types */
resolveUri: string => void, resolveUri: string => void,
isResolvingUri: boolean, isResolvingUri: boolean,
showPrice: boolean,
/* eslint-enable react/no-unused-prop-types */ /* eslint-enable react/no-unused-prop-types */
subscribed: boolean,
}; };
class FileCard extends React.PureComponent<Props> { class FileCard extends React.PureComponent<Props> {
static defaultProps = {
showPrice: true,
};
componentWillMount() { componentWillMount() {
this.resolve(this.props); this.resolve(this.props);
} }
@ -59,9 +56,20 @@ class FileCard extends React.PureComponent<Props> {
obscureNsfw, obscureNsfw,
claimIsMine, claimIsMine,
pending, pending,
showPrice, subscribed,
} = this.props; } = this.props;
if (!claim) {
return (
<div className="card card--small">
<div className="card--placeholder card__media" />
<div className="card--placeholder placeholder__title" />
<div className="card--placeholder placeholder__channel" />
<div className="card--placeholder placeholder__date" />
</div>
);
}
const shouldHide = !claimIsMine && !pending && obscureNsfw && metadata && metadata.nsfw; const shouldHide = !claimIsMine && !pending && obscureNsfw && metadata && metadata.nsfw;
if (shouldHide) { if (shouldHide) {
return null; return null;
@ -70,7 +78,8 @@ class FileCard extends React.PureComponent<Props> {
const uri = !pending ? normalizeURI(this.props.uri) : this.props.uri; const uri = !pending ? normalizeURI(this.props.uri) : this.props.uri;
const title = metadata && metadata.title ? metadata.title : uri; const title = metadata && metadata.title ? metadata.title : uri;
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null; const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const { height } = claim;
const handleContextMenu = event => { const handleContextMenu = event => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -97,10 +106,15 @@ class FileCard extends React.PureComponent<Props> {
<div className="card__subtitle"> <div className="card__subtitle">
{pending ? <div>Pending...</div> : <UriIndicator uri={uri} link />} {pending ? <div>Pending...</div> : <UriIndicator uri={uri} link />}
</div> </div>
<div className="card__file-properties"> <div className="card__subtitle card--space-between">
{showPrice && <FilePrice hideFree uri={uri} />} <DateTime timeAgo block={height} />
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{fileInfo && <Icon icon={icons.LOCAL} />} <div className="card__file-properties">
<FilePrice hideFree uri={uri} />
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{subscribed && <Icon icon={icons.HEART} />}
{fileInfo && <Icon icon={icons.LOCAL} />}
</div>
</div> </div>
</section> </section>
); );

View file

@ -11,6 +11,7 @@ import { selectShowNsfw } from 'redux/selectors/settings';
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
import { doClearPublish, doUpdatePublishForm } from 'redux/actions/publish'; import { doClearPublish, doUpdatePublishForm } from 'redux/actions/publish';
import { selectRewardContentClaimIds } from 'redux/selectors/content'; import { selectRewardContentClaimIds } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import FileTile from './view'; import FileTile from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -21,6 +22,7 @@ const select = (state, props) => ({
rewardedContentClaimIds: selectRewardContentClaimIds(state, props), rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
obscureNsfw: !selectShowNsfw(state), obscureNsfw: !selectShowNsfw(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
subscribed: makeSelectIsSubscribed(props.uri)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -10,9 +10,9 @@ import Button from 'component/button';
import classnames from 'classnames'; import classnames from 'classnames';
import FilePrice from 'component/filePrice'; import FilePrice from 'component/filePrice';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import DateTime from 'component/dateTime';
type Props = { type Props = {
showUri: boolean,
showLocal: boolean, showLocal: boolean,
obscureNsfw: boolean, obscureNsfw: boolean,
claimIsMine: boolean, claimIsMine: boolean,
@ -30,11 +30,11 @@ type Props = {
displayHiddenMessage?: boolean, displayHiddenMessage?: boolean,
displayDescription?: boolean, displayDescription?: boolean,
size: string, size: string,
subscribed: boolean,
}; };
class FileTile extends React.PureComponent<Props> { class FileTile extends React.PureComponent<Props> {
static defaultProps = { static defaultProps = {
showUri: false,
showLocal: false, showLocal: false,
displayDescription: true, displayDescription: true,
size: 'regular', size: 'regular',
@ -57,7 +57,6 @@ class FileTile extends React.PureComponent<Props> {
isResolvingUri, isResolvingUri,
navigate, navigate,
rewardedContentClaimIds, rewardedContentClaimIds,
showUri,
obscureNsfw, obscureNsfw,
claimIsMine, claimIsMine,
showLocal, showLocal,
@ -68,8 +67,27 @@ class FileTile extends React.PureComponent<Props> {
displayHiddenMessage, displayHiddenMessage,
displayDescription, displayDescription,
size, size,
subscribed,
} = this.props; } = this.props;
if (!claim && isResolvingUri) {
return (
<div
className={classnames('file-tile', {
'file-tile--small': size === 'small',
'file-tile--large': size === 'large',
})}
>
<div className="card--placeholder card__media" />
<div className="file-tile__info">
<div className="card--placeholder placeholder__title--tile" />
<div className="card--placeholder placeholder__channel" />
<div className="card--placeholder placeholder__date" />
</div>
</div>
);
}
const shouldHide = !claimIsMine && obscureNsfw && metadata && metadata.nsfw; const shouldHide = !claimIsMine && obscureNsfw && metadata && metadata.nsfw;
if (shouldHide) { if (shouldHide) {
return displayHiddenMessage ? ( return displayHiddenMessage ? (
@ -90,9 +108,10 @@ class FileTile extends React.PureComponent<Props> {
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
const onClick = () => navigate('/show', { uri }); const onClick = () => navigate('/show', { uri });
let height;
let name; let name;
if (claim) { if (isClaimed) {
({ name } = claim); ({ name, height } = claim);
} }
return !name && hideNoResult ? null : ( return !name && hideNoResult ? null : (
@ -108,43 +127,42 @@ class FileTile extends React.PureComponent<Props> {
> >
<CardMedia title={title || name} thumbnail={thumbnail} /> <CardMedia title={title || name} thumbnail={thumbnail} />
<div className="file-tile__info"> <div className="file-tile__info">
{isResolvingUri && <div className="file-tile__title">{__('Loading...')}</div>} <div className="file-tile__title">
{!isResolvingUri && ( <TruncatedText text={title || name} lines={size === 'small' ? 2 : 3} />
</div>
<div className="card__subtitle">
<UriIndicator uri={uri} link />
</div>
<div className="card__subtitle card--space-between">
<DateTime timeAgo block={height} />
<div className="card__file-properties">
<FilePrice hideFree uri={uri} />
{subscribed && <Icon icon={icons.HEART} />}
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{showLocal && isDownloaded && <Icon icon={icons.LOCAL} />}
</div>
</div>
{displayDescription && (
<div className="card__subtext">
<TruncatedText text={description} lines={size === 'large' ? 4 : 3} />
</div>
)}
{!name && (
<React.Fragment> <React.Fragment>
<div className="file-tile__title"> {__('This location is unused.')}{' '}
<TruncatedText text={title || name} lines={size === 'small' ? 2 : 3} /> <Button
</div> button="link"
<div className="card__subtitle"> label={__('Put something here!')}
{showUri ? uri : <UriIndicator uri={uri} link />} onClick={e => {
</div> // avoid navigating to /show from clicking on the section
<div className="card__file-properties"> e.stopPropagation();
<FilePrice hideFree uri={uri} /> // strip prefix from the Uri and use that as navigation parameter
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />} const nameFromUri = uri.replace(/lbry:\/\//g, '').replace(/-/g, ' ');
{showLocal && isDownloaded && <Icon icon={icons.LOCAL} />} clearPublish(); // to remove any existing publish data
</div> updatePublishForm({ name: nameFromUri }); // to populate the name
{displayDescription && ( navigate('/publish');
<div className="card__subtext"> }}
<TruncatedText text={description} lines={size === 'large' ? 4 : 3} /> />
</div>
)}
{!name && (
<React.Fragment>
{__('This location is unused.')}{' '}
<Button
button="link"
label={__('Put something here!')}
onClick={e => {
// avoid navigating to /show from clicking on the section
e.stopPropagation();
// strip prefix from the Uri and use that as navigation parameter
const nameFromUri = uri.replace(/lbry:\/\//g, '').replace(/-/g, ' ');
clearPublish(); // to remove any existing publish data
updatePublishForm({ name: nameFromUri }); // to populate the name
navigate('/publish');
}}
/>
</React.Fragment>
)}
</React.Fragment> </React.Fragment>
)} )}
</div> </div>

View file

@ -2,7 +2,6 @@
import React from 'react'; import React from 'react';
import FileTile from 'component/fileTile'; import FileTile from 'component/fileTile';
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import Spinner from 'component/spinner';
type Props = { type Props = {
uri: string, uri: string,
@ -58,6 +57,7 @@ export default class RecommendedContent extends React.PureComponent<Props> {
<FileTile <FileTile
size="small" size="small"
hideNoResult hideNoResult
showLocal
displayDescription={false} displayDescription={false}
key={recommendedUri} key={recommendedUri}
uri={recommendedUri} uri={recommendedUri}
@ -66,7 +66,6 @@ export default class RecommendedContent extends React.PureComponent<Props> {
{recommendedContent && {recommendedContent &&
!recommendedContent.length && !recommendedContent.length &&
!isSearching && <div className="card__subtitle">No related content found</div>} !isSearching && <div className="card__subtitle">No related content found</div>}
{isSearching && <Spinner type="small" />}
</section> </section>
); );
} }

View file

@ -18,7 +18,6 @@ const select = state => {
const perform = dispatch => ({ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)), navigate: (path, params) => dispatch(doNavigate(path, params)),
clearHistoryUri: uri => dispatch(doClearContentHistoryUri(uri)), clearHistoryUri: uri => dispatch(doClearContentHistoryUri(uri)),
}); });
export default connect( export default connect(

View file

@ -43,9 +43,13 @@ type Props = {
fetchFileInfo: string => void, fetchFileInfo: string => void,
fetchCostInfo: string => void, fetchCostInfo: string => void,
prepareEdit: ({}, string) => void, prepareEdit: ({}, string) => void,
setViewed: string => void,
autoplay: boolean,
setClientSetting: (string, string | boolean | number) => void,
/* eslint-disable react/no-unused-prop-types */
checkSubscription: (uri: string) => void, checkSubscription: (uri: string) => void,
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
setViewed: string => void, /* eslint-enable react/no-unused-prop-types */
}; };
class FilePage extends React.Component<Props> { class FilePage extends React.Component<Props> {
@ -183,7 +187,7 @@ class FilePage extends React.Component<Props> {
))} ))}
<div className="card__content"> <div className="card__content">
<div className="card__title__space-between"> <div className="card--space-between">
<h1>{title}</h1> <h1>{title}</h1>
<div className="card__title-identity-icons"> <div className="card__title-identity-icons">
{isRewardContent && ( {isRewardContent && (
@ -220,14 +224,12 @@ class FilePage extends React.Component<Props> {
onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })} onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })}
/> />
)} )}
{speechShareable && ( <Button
<Button button="alt"
button="alt" icon={icons.GLOBE}
icon={icons.GLOBE} label={__('Share')}
label={__('Share')} onClick={() => openModal({ id: MODALS.SOCIAL_SHARE }, { uri, speechShareable })}
onClick={() => openModal({ id: MODALS.SOCIAL_SHARE }, { uri })} />
/>
)}
</div> </div>
<div className="card__actions"> <div className="card__actions">

View file

@ -3,6 +3,7 @@ import {
selectAllClaimsByChannel, selectAllClaimsByChannel,
selectClaimsById, selectClaimsById,
selectAllFetchingChannelClaims, selectAllFetchingChannelClaims,
makeSelectClaimForUri,
} from 'lbry-redux'; } from 'lbry-redux';
// get the entire subscriptions state // get the entire subscriptions state
@ -71,3 +72,13 @@ export const selectSubscriptionsBeingFetched = createSelector(
return fetchingSubscriptionMap; return fetchingSubscriptionMap;
} }
); );
export const makeSelectIsSubscribed = uri =>
createSelector(selectSubscriptions, makeSelectClaimForUri(uri), (subscriptions, claim) => {
if (!claim || !claim.channel_name) {
return false;
}
const channelUri = `${claim.channel_name}#${claim.value.publisherSignature.certificateId}`;
return subscriptions.some(sub => sub.uri === channelUri);
});

View file

@ -32,3 +32,4 @@
@import 'component/_item-list.scss'; @import 'component/_item-list.scss';
@import 'component/_time.scss'; @import 'component/_time.scss';
@import 'component/_icon.scss'; @import 'component/_icon.scss';
@import 'component/_placeholder.scss';

View file

@ -88,20 +88,16 @@
// FileCard is slightly different where the title is only slightly bigger than the subtitle // FileCard is slightly different where the title is only slightly bigger than the subtitle
// Slightly bigger than 2 lines for consistent channel placement // Slightly bigger than 2 lines for consistent channel placement
font-size: 1.1em; font-size: 1.1em;
height: 4em; height: 3.3em;
@media only screen and (min-width: $large-breakpoint) { @media only screen and (min-width: $large-breakpoint) {
font-size: 1.3em; font-size: 1.3em;
} }
} }
.card__title__space-between { .card--space-between {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
.icon {
margin: 0 $spacing-vertical * 1/3;
}
} }
.card__title-identity-icons { .card__title-identity-icons {
@ -114,6 +110,7 @@
color: var(--card-text-color); color: var(--card-text-color);
font-size: 1em; font-size: 1em;
line-height: 1em; line-height: 1em;
padding-top: $spacing-vertical * 1/3;
@media (min-width: $large-breakpoint) { @media (min-width: $large-breakpoint) {
font-size: 1.2em; font-size: 1.2em;
@ -142,8 +139,11 @@
.card__file-properties { .card__file-properties {
display: flex; display: flex;
align-items: center; align-items: center;
padding-top: $spacing-vertical * 1/3;
color: var(--card-text-color); color: var(--card-text-color);
.icon + .icon {
margin-left: $spacing-vertical * 1/3;
}
} }
// .card-media__internal__links should always be inside a card // .card-media__internal__links should always be inside a card

View file

@ -45,6 +45,7 @@
} }
.file-tile__title { .file-tile__title {
font-size: 1.1em;
} }
.file-tile--small { .file-tile--small {
@ -65,6 +66,7 @@
.file-tile__info { .file-tile__info {
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
margin-left: $spacing-vertical * 2/3; margin-left: $spacing-vertical * 2/3;
max-width: 500px; max-width: 500px;

View file

@ -0,0 +1,38 @@
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.card--placeholder {
animation: pulse 2s infinite ease-in-out;
background: #eeeeee;
}
// Individual items we need a placeholder for
// FileCard
.placeholder__title {
margin-top: $spacing-vertical * 1/3;
height: 3em;
}
.placeholder__channel {
margin-top: $spacing-vertical * 1/3;
height: 1em;
width: 70%;
}
.placeholder__date {
height: 1em;
margin-top: $spacing-vertical * 1/3;
width: 50%;
}
// FileTile
.placeholder__title--tile {
height: 3em;
}

View file

@ -5662,9 +5662,9 @@ lbry-redux@lbryio/lbry-redux:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"
lbry-redux@lbryio/lbry-redux#c079b108c3bc4ba2b4fb85fb112b52cfc040c301: lbry-redux@lbryio/lbry-redux#4ee6c376e5f2c3e3e96d199a56970e2621a84af1:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/c079b108c3bc4ba2b4fb85fb112b52cfc040c301" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/4ee6c376e5f2c3e3e96d199a56970e2621a84af1"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"