Actually hide NSFW content #1748

Merged
neb-b merged 8 commits from hide-nsfw into master 2018-07-13 20:31:15 +02:00
16 changed files with 690 additions and 676 deletions

View file

@ -48,7 +48,7 @@
"formik": "^0.10.4", "formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2", "hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1", "keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#a0d2d1ac532ade639d39c92f79678ac26e904dfd", "lbry-redux": "lbryio/lbry-redux#177ef2c1916f9672e713267500e447d671ae1bc3",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"mime": "^2.3.1", "mime": "^2.3.1",
"mixpanel-browser": "^2.17.1", "mixpanel-browser": "^2.17.1",

View file

@ -4,46 +4,20 @@ import classnames from 'classnames';
type Props = { type Props = {
thumbnail: ?string, // externally sourced image thumbnail: ?string, // externally sourced image
nsfw: ?boolean,
}; };
const autoThumbColors = [
'purple',
'red',
'pink',
'indigo',
'blue',
'light-blue',
'cyan',
'teal',
'green',
'yellow',
'orange',
];
class CardMedia extends React.PureComponent<Props> { class CardMedia extends React.PureComponent<Props> {
getAutoThumbClass = () => autoThumbColors[Math.floor(Math.random() * autoThumbColors.length)];
render() { render() {
const { thumbnail, nsfw } = this.props; const { thumbnail } = this.props;
const generateAutothumb = !thumbnail && !nsfw;
let autoThumbClass;
if (generateAutothumb) {
autoThumbClass = `card__media--autothumb.${this.getAutoThumbClass()}`;
}
return ( return (
<div <div
style={thumbnail && !nsfw ? { backgroundImage: `url('${thumbnail}')` } : {}} style={thumbnail ? { backgroundImage: `url('${thumbnail}')` } : {}}
className={classnames('card__media', autoThumbClass, { className={classnames('card__media', {
'card__media--no-img': !thumbnail || nsfw, 'card__media--no-img': !thumbnail,
'card__media--nsfw': nsfw,
})} })}
> >
{(!thumbnail || nsfw) && ( {!thumbnail && <span className="card__media-text">LBRY</span>}
<span className="card__media-text">{nsfw ? __('NSFW') : 'LBRY'}</span>
)}
</div> </div>
); );
} }

View file

@ -249,7 +249,7 @@ class CategoryList extends React.PureComponent<Props, State> {
)} )}
</div> </div>
{obscureNsfw && isCommunityTopBids ? ( {obscureNsfw && isCommunityTopBids ? (
<div className="card__content help"> <div className="card-row__message help">
{__( {__(
'The community top bids section is only visible if you allow mature content in the app. You can change your content viewing preferences' 'The community top bids section is only visible if you allow mature content in the app. You can change your content viewing preferences'
)}{' '} )}{' '}

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { normalizeURI, convertToShareLink } from 'lbry-redux'; import { normalizeURI, convertToShareLink } from 'lbry-redux';
import Button from 'component/button'; import type { Claim, Metadata } from 'types/claim';
import CardMedia from 'component/cardMedia'; import CardMedia from 'component/cardMedia';
import TruncatedText from 'component/common/truncated-text'; import TruncatedText from 'component/common/truncated-text';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
@ -14,9 +14,9 @@ import { openCopyLinkMenu } from '../../util/contextMenu';
// TODO: iron these out // TODO: iron these out
type Props = { type Props = {
uri: string, uri: string,
claim: ?{ claim_id: string }, claim: ?Claim,
fileInfo: ?{}, fileInfo: ?{},
metadata: ?{ nsfw: boolean, title: string, thumbnail: ?string }, metadata: ?Metadata,
navigate: (string, ?{}) => void, navigate: (string, ?{}) => void,
rewardedContentClaimIds: Array<string>, rewardedContentClaimIds: Array<string>,
obscureNsfw: boolean, obscureNsfw: boolean,
@ -62,10 +62,15 @@ class FileCard extends React.PureComponent<Props> {
showPrice, showPrice,
pending, pending,
} = this.props; } = this.props;
const shouldHide = !claimIsMine && !pending && obscureNsfw && metadata && metadata.nsfw;
if (shouldHide) {
return null;
}
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 shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw && !claimIsMine;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
const handleContextMenu = event => { const handleContextMenu = event => {
event.preventDefault(); event.preventDefault();
@ -86,46 +91,26 @@ class FileCard extends React.PureComponent<Props> {
})} })}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
<CardMedia nsfw={shouldObscureNsfw} thumbnail={thumbnail} /> <CardMedia thumbnail={thumbnail} />
<div className="card-media__internal-links">{showPrice && <FilePrice uri={uri} />}</div> <div className="card-media__internal-links">{showPrice && <FilePrice uri={uri} />}</div>
<div className="card__title-identity">
{shouldObscureNsfw ? ( <div className="card__title--small">
<div className="card__title-identity"> <TruncatedText lines={3}>{title}</TruncatedText>
<div className="card__title--small">
<TruncatedText lines={3}>
{__('This content is obscured because it is NSFW. You can change this in ')}
<Button
button="link"
label={__('Settings.')}
onClick={e => {
// Don't propagate to the onClick handler of parent element
e.stopPropagation();
navigate('/settings');
}}
/>
</TruncatedText>
</div>
</div> </div>
) : ( <div className="card__subtitle">
<div className="card__title-identity"> {pending ? (
<div className="card__title--small"> <div>Pending...</div>
<TruncatedText lines={3}>{title}</TruncatedText> ) : (
</div> <React.Fragment>
<div className="card__subtitle"> <UriIndicator uri={uri} link />
{pending ? ( <div>
<div>Pending...</div> {isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
) : ( {fileInfo && <Icon icon={icons.LOCAL} />}
<React.Fragment> </div>
<UriIndicator uri={uri} link /> </React.Fragment>
<div> )}
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{fileInfo && <Icon icon={icons.LOCAL} />}
</div>
</React.Fragment>
)}
</div>
</div> </div>
)} </div>
</section> </section>
); );
/* eslint-enable jsx-a11y/click-events-have-key-events */ /* eslint-enable jsx-a11y/click-events-have-key-events */

View file

@ -1,8 +1,9 @@
// @flow // @flow
import React from 'react'; import * as React from 'react';
import { parseURI } from 'lbry-redux';
import FileTile from 'component/fileTile'; import FileTile from 'component/fileTile';
import ChannelTile from 'component/channelTile'; import ChannelTile from 'component/channelTile';
import { parseURI } from 'lbry-redux'; import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
const NoResults = () => <div className="file-tile">{__('No results')}</div>; const NoResults = () => <div className="file-tile">{__('No results')}</div>;
@ -11,7 +12,6 @@ type Props = {
isSearching: boolean, isSearching: boolean,
uris: ?Array<string>, uris: ?Array<string>,
downloadUris: ?Array<string>, downloadUris: ?Array<string>,
resultCount: number,
}; };
class FileListSearch extends React.PureComponent<Props> { class FileListSearch extends React.PureComponent<Props> {
@ -33,36 +33,37 @@ class FileListSearch extends React.PureComponent<Props> {
return ( return (
query && ( query && (
<div className="search__results"> <React.Fragment>
<div className="search-result__row"> <div className="search__results">
<div className="file-list__header">{__('Content')}</div> <div className="search-result__row">
{!isSearching && <div className="file-list__header">{__('Content')}</div>
(fileResults.length ? ( <HiddenNsfwClaims uris={uris} />
{!isSearching && fileResults.length ? (
fileResults.map(uri => <FileTile key={uri} uri={uri} />) fileResults.map(uri => <FileTile key={uri} uri={uri} />)
) : ( ) : (
<NoResults /> <NoResults />
))} )}
</div> </div>
<div className="search-result__row"> <div className="search-result__row">
<div className="file-list__header">{__('Channels')}</div> <div className="file-list__header">{__('Channels')}</div>
{!isSearching && {!isSearching && channelResults.length ? (
(channelResults.length ? (
channelResults.map(uri => <ChannelTile key={uri} uri={uri} />) channelResults.map(uri => <ChannelTile key={uri} uri={uri} />)
) : ( ) : (
<NoResults /> <NoResults />
))} )}
</div> </div>
<div className="search-result__row"> <div className="search-result__row">
<div className="file-list__header">{__('Your downloads')}</div> <div className="file-list__header">{__('Your downloads')}</div>
{downloadUris && downloadUris.length ? ( {downloadUris && downloadUris.length ? (
downloadUris.map(uri => <FileTile hideNoResult key={uri} uri={uri} />) downloadUris.map(uri => <FileTile hideNoResult key={uri} uri={uri} />)
) : ( ) : (
<NoResults /> <NoResults />
)} )}
</div>
</div> </div>
</div> </React.Fragment>
) )
); );
} }

View file

@ -30,4 +30,7 @@ const perform = dispatch => ({
updatePublishForm: value => dispatch(doUpdatePublishForm(value)), updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
}); });
export default connect(select, perform)(FileTile); export default connect(
select,
perform
)(FileTile);

View file

@ -1,6 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import type { Claim, Metadata } from 'types/claim';
import { normalizeURI, parseURI } from 'lbry-redux'; import { normalizeURI, parseURI } from 'lbry-redux';
import CardMedia from 'component/cardMedia'; import CardMedia from 'component/cardMedia';
import TruncatedText from 'component/common/truncated-text'; import TruncatedText from 'component/common/truncated-text';
@ -19,20 +20,14 @@ type Props = {
uri: string, uri: string,
isResolvingUri: boolean, isResolvingUri: boolean,
rewardedContentClaimIds: Array<string>, rewardedContentClaimIds: Array<string>,
claim: ?{ claim: ?Claim,
name: string, metadata: ?Metadata,
channel_name: string,
claim_id: string,
},
metadata: ?{
title: ?string,
thumbnail: ?string,
},
resolveUri: string => void, resolveUri: string => void,
navigate: (string, ?{}) => void, navigate: (string, ?{}) => void,
clearPublish: () => void, clearPublish: () => void,
updatePublishForm: ({}) => void, updatePublishForm: ({}) => void,
hideNoResult: boolean, // don't show the tile if there is no claim at this uri hideNoResult: boolean, // don't show the tile if there is no claim at this uri
displayHiddenMessage?: boolean,
}; };
class FileTile extends React.PureComponent<Props> { class FileTile extends React.PureComponent<Props> {
@ -68,8 +63,20 @@ class FileTile extends React.PureComponent<Props> {
clearPublish, clearPublish,
updatePublishForm, updatePublishForm,
hideNoResult, hideNoResult,
displayHiddenMessage,
} = this.props; } = this.props;
const shouldHide = !claimIsMine && obscureNsfw && metadata && metadata.nsfw;
if (shouldHide) {
return displayHiddenMessage ? (
<span className="help">
{__('This file is hidden because it is marked NSFW. Update your')}{' '}
<Button button="link" navigate="/settings" label={__('content viewing preferences')} />{' '}
{__('to see it')}.
</span>
) : null;
}
const uri = normalizeURI(this.props.uri); const uri = normalizeURI(this.props.uri);
const isClaimed = !!claim; const isClaimed = !!claim;
const description = isClaimed && metadata && metadata.description ? metadata.description : ''; const description = isClaimed && metadata && metadata.description ? metadata.description : '';
@ -77,8 +84,6 @@ class FileTile extends React.PureComponent<Props> {
isClaimed && metadata && metadata.title ? metadata.title : parseURI(uri).contentName; isClaimed && metadata && metadata.title ? metadata.title : parseURI(uri).contentName;
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 = claim && rewardedContentClaimIds.includes(claim.claim_id);
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw && !claimIsMine;
const onClick = () => navigate('/show', { uri }); const onClick = () => navigate('/show', { uri });
let name; let name;
@ -98,7 +103,7 @@ class FileTile extends React.PureComponent<Props> {
role="button" role="button"
tabIndex="0" tabIndex="0"
> >
<CardMedia title={title || name} thumbnail={thumbnail} nsfw={shouldObscureNsfw} /> <CardMedia title={title || name} thumbnail={thumbnail} />
<div className="file-tile__info"> <div className="file-tile__info">
{isResolvingUri && <div className="card__title--small">{__('Loading...')}</div>} {isResolvingUri && <div className="card__title--small">{__('Loading...')}</div>}
{!isResolvingUri && ( {!isResolvingUri && (

View file

@ -0,0 +1,30 @@
import { connect } from 'react-redux';
import { makeSelectNsfwCountForChannel, makeSelectNsfwCountFromUris, parseURI } from 'lbry-redux';
import { selectShowNsfw } from 'redux/selectors/settings';
import HiddenNsfwClaims from './view';
const select = (state, props) => {
const { uri, uris } = props;
let numberOfNsfwClaims;
if (uri) {
const { isChannel } = parseURI(uri);
numberOfNsfwClaims = isChannel
? makeSelectNsfwCountForChannel(uri)(state)
: makeSelectNsfwCountFromUris([uri])(state);
} else if (uris) {
numberOfNsfwClaims = makeSelectNsfwCountFromUris(uris)(state);
}
return {
numberOfNsfwClaims,
obscureNsfw: !selectShowNsfw(state),
};
};
const perform = () => ({});
export default connect(
select,
perform
)(HiddenNsfwClaims);

View file

@ -0,0 +1,23 @@
// @flow
import React from 'react';
import Button from 'component/button';
type Props = {
numberOfNsfwClaims: number,
obscureNsfw: boolean,
className: ?string,
};
export default (props: Props) => {
const { numberOfNsfwClaims, obscureNsfw, className } = props;
return (
obscureNsfw &&
Boolean(numberOfNsfwClaims) && (
<div className={className || 'help'}>
{numberOfNsfwClaims} {numberOfNsfwClaims > 1 ? __('files') : __('file')}{' '}
{__('hidden due to your')}{' '}
<Button button="link" navigate="/settings" label={__('content viewing preferences')} />.
</div>
)
);
};

View file

@ -5,6 +5,7 @@ import {
makeSelectClaimsInChannelForCurrentPage, makeSelectClaimsInChannelForCurrentPage,
makeSelectFetchingChannelClaims, makeSelectFetchingChannelClaims,
makeSelectCurrentParam, makeSelectCurrentParam,
makeSelectClaimIsMine,
selectCurrentParams, selectCurrentParams,
} from 'lbry-redux'; } from 'lbry-redux';
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
@ -18,6 +19,7 @@ const select = (state, props) => ({
page: makeSelectCurrentParam('page')(state), page: makeSelectCurrentParam('page')(state),
params: selectCurrentParams(state), params: selectCurrentParams(state),
totalPages: makeSelectTotalPagesForChannel(props.uri)(state), totalPages: makeSelectTotalPagesForChannel(props.uri)(state),
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
@ -26,4 +28,7 @@ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)), navigate: (path, params) => dispatch(doNavigate(path, params)),
}); });
export default connect(select, perform)(ChannelPage); export default connect(
select,
perform
)(ChannelPage);

View file

@ -7,6 +7,7 @@ import SubscribeButton from 'component/subscribeButton';
import ViewOnWebButton from 'component/viewOnWebButton'; import ViewOnWebButton from 'component/viewOnWebButton';
import Page from 'component/page'; import Page from 'component/page';
import FileList from 'component/fileList'; import FileList from 'component/fileList';
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
type Props = { type Props = {
@ -16,7 +17,8 @@ type Props = {
fetching: boolean, fetching: boolean,
params: { page: number }, params: { page: number },
claim: Claim, claim: Claim,
claimsInChannel: Array<{}>, claimsInChannel: Array<Claim>,
channelIsMine: boolean,
fetchClaims: (string, number) => void, fetchClaims: (string, number) => void,
fetchClaimCount: string => void, fetchClaimCount: string => void,
navigate: (string, {}) => void, navigate: (string, {}) => void,
@ -51,9 +53,10 @@ class ChannelPage extends React.PureComponent<Props> {
paginate(e: SyntheticKeyboardEvent<*>, totalPages: number) { paginate(e: SyntheticKeyboardEvent<*>, totalPages: number) {
// Change page if enter was pressed, and the given page is between // Change page if enter was pressed, and the given page is between
// the first and the last. // the first and the last.
const pageFromInput = Number(e.target.value); const pageFromInput = Number(e.currentTarget.value);
if ( if (
pageFromInput &&
e.keyCode === 13 && e.keyCode === 13 &&
!Number.isNaN(pageFromInput) && !Number.isNaN(pageFromInput) &&
pageFromInput > 0 && pageFromInput > 0 &&
@ -64,7 +67,7 @@ class ChannelPage extends React.PureComponent<Props> {
} }
render() { render() {
const { fetching, claimsInChannel, claim, page, totalPages } = this.props; const { uri, fetching, claimsInChannel, claim, page, totalPages, channelIsMine } = this.props;
const { name, permanent_url: permanentUrl, claim_id: claimId } = claim; const { name, permanent_url: permanentUrl, claim_id: claimId } = claim;
const currentPage = parseInt((page || 1) - 1, 10); const currentPage = parseInt((page || 1) - 1, 10);
@ -116,6 +119,7 @@ class ChannelPage extends React.PureComponent<Props> {
/> />
</FormRow> </FormRow>
)} )}
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
</Page> </Page>
); );
} }

View file

@ -73,7 +73,7 @@ class SearchPage extends React.PureComponent<Props> {
<Icon icon={icons.HELP} /> <Icon icon={icons.HELP} />
</ToolTip> </ToolTip>
</div> </div>
<FileTile fullWidth uri={normalizeURI(query)} showUri /> <FileTile fullWidth showUri displayHiddenMessage uri={normalizeURI(query)} />
</React.Fragment> </React.Fragment>
)} )}
<FileListSearch query={query} /> <FileListSearch query={query} />

View file

@ -7,6 +7,7 @@ import Button from 'component/button';
import FileList from 'component/fileList'; import FileList from 'component/fileList';
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
type Props = { type Props = {
doFetchClaimsByChannel: (string, number) => void, doFetchClaimsByChannel: (string, number) => void,
@ -51,9 +52,10 @@ export default class extends React.PureComponent<Props> {
if (claim.claims.length) { if (claim.claims.length) {
subscriptionClaimMap[claim.uri] = 1; subscriptionClaimMap[claim.uri] = 1;
} else if (isDev) { } else if (isDev) {
console.error( console
`claim for ${claim.uri} was added to byId in redux but there are no loaded fetched claims` .error
); // `claim for ${claim.uri} was added to byId in redux but there are no loaded fetched claims`
();
} }
}); });
@ -72,8 +74,11 @@ export default class extends React.PureComponent<Props> {
claimList = claimList.concat(claimData.claims); claimList = claimList.concat(claimData.claims);
}); });
const subscriptionUris = claimList.map(claim => `lbry://${claim.name}#${claim.claim_id}`);
return ( return (
<Page notContained loading={isFetchingSubscriptions}> <Page notContained loading={isFetchingSubscriptions}>
<HiddenNsfwClaims uris={subscriptionUris} />
{!subscriptions.length && ( {!subscriptions.length && (
<div className="page__empty"> <div className="page__empty">
{__("It looks like you aren't subscribed to any channels yet.")} {__("It looks like you aren't subscribed to any channels yet.")}

View file

@ -281,14 +281,6 @@
&:last-of-type { &:last-of-type {
padding-bottom: $spacing-vertical * 2/3; padding-bottom: $spacing-vertical * 2/3;
} }
// This is only for the text that is displayed when a user has nsfw hidden
// It is not used anywhere else in the app and is needed due to the css related to
// the content that scrolls off the edge of the screen
.card__content.help {
padding: 0 $spacing-width;
white-space: normal;
}
} }
.card-row__header { .card-row__header {
@ -350,6 +342,11 @@
} }
} }
.card-row__message {
padding: 0 $spacing-width;
white-space: normal;
}
/* /*
How cards are displayed in lists How cards are displayed in lists
*/ */
@ -357,7 +354,7 @@
.card { .card {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
margin-bottom: 60px; margin-bottom: $spacing-vertical * 3/2;
@media only screen and (max-width: $medium-breakpoint) { @media only screen and (max-width: $medium-breakpoint) {
width: calc((100% / 4) - (60px / 4)); // 60px === 20px margin-right * three cards width: calc((100% / 4) - (60px / 4)); // 60px === 20px margin-right * three cards

View file

@ -1,5 +1,13 @@
// @flow // @flow
// Currently incomplete
export type Metadata = {
nsfw: boolean,
title: string,
thumbnail: ?string,
description: ?string,
};
// Actual claim type has more values than this // Actual claim type has more values than this
// Add them as they are used // Add them as they are used
export type Claim = { export type Claim = {
@ -22,9 +30,10 @@ export type Claim = {
nout: number, nout: number,
signature_is_valid: boolean, signature_is_valid: boolean,
valid_at_height: number, valid_at_height: number,
value: { value: ?{
publisherSignature: ?{ publisherSignature: ?{
certificateId: ?string, certificateId: ?string,
}, },
stream: ?Metadata,
}, },
}; };

1067
yarn.lock

File diff suppressed because it is too large Load diff