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",
"hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#a0d2d1ac532ade639d39c92f79678ac26e904dfd",
"lbry-redux": "lbryio/lbry-redux#177ef2c1916f9672e713267500e447d671ae1bc3",
"localforage": "^1.7.1",
"mime": "^2.3.1",
"mixpanel-browser": "^2.17.1",

View file

@ -4,46 +4,20 @@ import classnames from 'classnames';
type Props = {
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> {
getAutoThumbClass = () => autoThumbColors[Math.floor(Math.random() * autoThumbColors.length)];
render() {
const { thumbnail, nsfw } = this.props;
const generateAutothumb = !thumbnail && !nsfw;
let autoThumbClass;
if (generateAutothumb) {
autoThumbClass = `card__media--autothumb.${this.getAutoThumbClass()}`;
}
const { thumbnail } = this.props;
return (
<div
style={thumbnail && !nsfw ? { backgroundImage: `url('${thumbnail}')` } : {}}
className={classnames('card__media', autoThumbClass, {
'card__media--no-img': !thumbnail || nsfw,
'card__media--nsfw': nsfw,
style={thumbnail ? { backgroundImage: `url('${thumbnail}')` } : {}}
className={classnames('card__media', {
'card__media--no-img': !thumbnail,
})}
>
{(!thumbnail || nsfw) && (
<span className="card__media-text">{nsfw ? __('NSFW') : 'LBRY'}</span>
)}
{!thumbnail && <span className="card__media-text">LBRY</span>}
</div>
);
}

View file

@ -249,7 +249,7 @@ class CategoryList extends React.PureComponent<Props, State> {
)}
</div>
{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'
)}{' '}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
// @flow
import * as React from 'react';
import * as icons from 'constants/icons';
import type { Claim, Metadata } from 'types/claim';
import { normalizeURI, parseURI } from 'lbry-redux';
import CardMedia from 'component/cardMedia';
import TruncatedText from 'component/common/truncated-text';
@ -19,20 +20,14 @@ type Props = {
uri: string,
isResolvingUri: boolean,
rewardedContentClaimIds: Array<string>,
claim: ?{
name: string,
channel_name: string,
claim_id: string,
},
metadata: ?{
title: ?string,
thumbnail: ?string,
},
claim: ?Claim,
metadata: ?Metadata,
resolveUri: string => void,
navigate: (string, ?{}) => void,
clearPublish: () => void,
updatePublishForm: ({}) => void,
hideNoResult: boolean, // don't show the tile if there is no claim at this uri
displayHiddenMessage?: boolean,
};
class FileTile extends React.PureComponent<Props> {
@ -68,8 +63,20 @@ class FileTile extends React.PureComponent<Props> {
clearPublish,
updatePublishForm,
hideNoResult,
displayHiddenMessage,
} = 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 isClaimed = !!claim;
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;
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw && !claimIsMine;
const onClick = () => navigate('/show', { uri });
let name;
@ -98,7 +103,7 @@ class FileTile extends React.PureComponent<Props> {
role="button"
tabIndex="0"
>
<CardMedia title={title || name} thumbnail={thumbnail} nsfw={shouldObscureNsfw} />
<CardMedia title={title || name} thumbnail={thumbnail} />
<div className="file-tile__info">
{isResolvingUri && <div className="card__title--small">{__('Loading...')}</div>}
{!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,
makeSelectFetchingChannelClaims,
makeSelectCurrentParam,
makeSelectClaimIsMine,
selectCurrentParams,
} from 'lbry-redux';
import { doNavigate } from 'redux/actions/navigation';
@ -18,6 +19,7 @@ const select = (state, props) => ({
page: makeSelectCurrentParam('page')(state),
params: selectCurrentParams(state),
totalPages: makeSelectTotalPagesForChannel(props.uri)(state),
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
});
const perform = dispatch => ({
@ -26,4 +28,7 @@ const perform = dispatch => ({
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 Page from 'component/page';
import FileList from 'component/fileList';
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
import type { Claim } from 'types/claim';
type Props = {
@ -16,7 +17,8 @@ type Props = {
fetching: boolean,
params: { page: number },
claim: Claim,
claimsInChannel: Array<{}>,
claimsInChannel: Array<Claim>,
channelIsMine: boolean,
fetchClaims: (string, number) => void,
fetchClaimCount: string => void,
navigate: (string, {}) => void,
@ -51,9 +53,10 @@ class ChannelPage extends React.PureComponent<Props> {
paginate(e: SyntheticKeyboardEvent<*>, totalPages: number) {
// Change page if enter was pressed, and the given page is between
// the first and the last.
const pageFromInput = Number(e.target.value);
const pageFromInput = Number(e.currentTarget.value);
if (
pageFromInput &&
e.keyCode === 13 &&
!Number.isNaN(pageFromInput) &&
pageFromInput > 0 &&
@ -64,7 +67,7 @@ class ChannelPage extends React.PureComponent<Props> {
}
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 currentPage = parseInt((page || 1) - 1, 10);
@ -116,6 +119,7 @@ class ChannelPage extends React.PureComponent<Props> {
/>
</FormRow>
)}
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
</Page>
);
}

View file

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

View file

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

View file

@ -281,14 +281,6 @@
&:last-of-type {
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 {
@ -350,6 +342,11 @@
}
}
.card-row__message {
padding: 0 $spacing-width;
white-space: normal;
}
/*
How cards are displayed in lists
*/
@ -357,7 +354,7 @@
.card {
display: inline-block;
vertical-align: top;
margin-bottom: 60px;
margin-bottom: $spacing-vertical * 3/2;
@media only screen and (max-width: $medium-breakpoint) {
width: calc((100% / 4) - (60px / 4)); // 60px === 20px margin-right * three cards

View file

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

1067
yarn.lock

File diff suppressed because it is too large Load diff