adds user ability to block channels

small changes

blocked channels page

most features done

tweaks
This commit is contained in:
jessop 2019-07-08 16:54:58 -04:00
parent 223a68413f
commit 5ab165131f
21 changed files with 217 additions and 16 deletions

View file

@ -89,6 +89,14 @@ const analytics: Analytics = {
}); });
} }
}, },
channelBlockEvent: (uri, blocked, location) => {
if (analyticsEnabled) {
ReactGA.event({
category: blocked ? 'Channel-Hidden' : 'Channel-Unhidden',
action: uri,
});
}
},
}; };
// Initialize google analytics // Initialize google analytics

View file

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import { selectChannelIsBlocked, doToggleBlockChannel, doToast } from 'lbry-redux';
import BlockButton from './view';
const select = (state, props) => ({
channelIsBlocked: selectChannelIsBlocked(props.uri)(state),
});
export default connect(
select,
{
toggleBlockChannel: doToggleBlockChannel,
doToast,
}
)(BlockButton);

View file

@ -0,0 +1,40 @@
// @flow
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import React, { useRef } from 'react';
import Button from 'component/button';
import useHover from 'util/use-hover';
type Props = {
uri: string,
isSubscribed: boolean,
toggleBlockChannel: (uri: string) => void,
channelIsBlocked: boolean,
doToast: ({ message: string, linkText: string, linkTarget: string }) => void,
};
export default function BlockButton(props: Props) {
const { uri, toggleBlockChannel, channelIsBlocked, doToast } = props;
const blockRef = useRef();
const isHovering = useHover(blockRef);
const blockLabel = channelIsBlocked ? __('Blocked') : __('Block');
const blockedOverride = channelIsBlocked && isHovering && __('Unblock');
return (
<Button
ref={blockRef}
iconColor="red"
icon={blockedOverride ? ICONS.UNBLOCK : ICONS.BLOCK}
button={'alt'}
label={blockedOverride || blockLabel}
onClick={e => {
e.stopPropagation();
if (!channelIsBlocked) {
doToast({ message: `Blocked ${uri}`, linkText: 'Manage', linkTarget: `/${PAGES.BLOCKED}` });
}
toggleBlockChannel(uri);
}}
/>
);
}

View file

@ -6,6 +6,7 @@ import {
makeSelectFetchingChannelClaims, makeSelectFetchingChannelClaims,
makeSelectClaimIsMine, makeSelectClaimIsMine,
makeSelectTotalPagesForChannel, makeSelectTotalPagesForChannel,
selectChannelIsBlocked,
} from 'lbry-redux'; } from 'lbry-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import ChannelPage from './view'; import ChannelPage from './view';
@ -19,6 +20,7 @@ const select = (state, props) => {
fetching: makeSelectFetchingChannelClaims(props.uri)(state), fetching: makeSelectFetchingChannelClaims(props.uri)(state),
totalPages: makeSelectTotalPagesForChannel(props.uri, PAGE_SIZE)(state), totalPages: makeSelectTotalPagesForChannel(props.uri, PAGE_SIZE)(state),
channelIsMine: makeSelectClaimIsMine(props.uri)(state), channelIsMine: makeSelectClaimIsMine(props.uri)(state),
channelIsBlocked: selectChannelIsBlocked(props.uri)(state),
}; };
}; };

View file

@ -12,12 +12,13 @@ type Props = {
fetching: boolean, fetching: boolean,
params: { page: number }, params: { page: number },
claimsInChannel: Array<StreamClaim>, claimsInChannel: Array<StreamClaim>,
channelIsBlocked: boolean,
channelIsMine: boolean, channelIsMine: boolean,
fetchClaims: (string, number) => void, fetchClaims: (string, number) => void,
}; };
function ChannelContent(props: Props) { function ChannelContent(props: Props) {
const { uri, fetching, claimsInChannel, totalPages, channelIsMine, fetchClaims } = props; const { uri, fetching, claimsInChannel, totalPages, channelIsMine, channelIsBlocked, fetchClaims } = props;
const hasContent = Boolean(claimsInChannel && claimsInChannel.length); const hasContent = Boolean(claimsInChannel && claimsInChannel.length);
return ( return (
<Fragment> <Fragment>
@ -27,21 +28,30 @@ function ChannelContent(props: Props) {
</section> </section>
)} )}
{!fetching && !hasContent && ( {!fetching && !hasContent && !channelIsBlocked && (
<div className="card--section"> <div className="card--section">
<h2 className="help">{__("This channel hasn't uploaded anything.")}</h2> <h2 className="help">{__("This channel hasn't uploaded anything.")}</h2>
</div> </div>
)} )}
{!fetching && channelIsBlocked && (
<div className="card--section">
<h2 className="card__content help">{__('You have blocked this channel content.')}</h2>
</div>
)}
{!channelIsMine && <HiddenNsfwClaims className="card__subtitle" uri={uri} />} {!channelIsMine && <HiddenNsfwClaims className="card__subtitle" uri={uri} />}
{hasContent && <ClaimList header={false} uris={claimsInChannel.map(claim => claim.permanent_url)} />} {hasContent && !channelIsBlocked && (
<ClaimList header={false} uris={claimsInChannel.map(claim => claim.permanent_url)} />
<Paginate )}
onPageChange={page => fetchClaims(uri, page)} {!channelIsBlocked && (
totalPages={totalPages} <Paginate
loading={fetching && !hasContent} onPageChange={page => fetchClaims(uri, page)}
/> totalPages={totalPages}
loading={fetching && !hasContent}
/>
)}
</Fragment> </Fragment>
); );
} }

View file

@ -8,10 +8,13 @@ import {
makeSelectThumbnailForUri, makeSelectThumbnailForUri,
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
selectBlockedChannels,
selectChannelIsBlocked,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings'; import { selectShowNsfw } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content'; import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import ClaimPreview from './view'; import ClaimPreview from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -25,7 +28,10 @@ const select = (state, props) => ({
nsfw: makeSelectClaimIsNsfw(props.uri)(state), nsfw: makeSelectClaimIsNsfw(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state), blackListedOutpoints: selectBlackListedOutpoints(state),
filteredOutpoints: selectFilteredOutpoints(state), filteredOutpoints: selectFilteredOutpoints(state),
blockedChannelUris: selectBlockedChannels(state),
hasVisitedUri: makeSelectHasVisitedUri(props.uri)(state), hasVisitedUri: makeSelectHasVisitedUri(props.uri)(state),
channelIsBlocked: selectChannelIsBlocked(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -13,12 +13,14 @@ import FileProperties from 'component/fileProperties';
import ClaimTags from 'component/claimTags'; import ClaimTags from 'component/claimTags';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
import BlockButton from 'component/blockButton';
import Button from 'component/button'; import Button from 'component/button';
type Props = { type Props = {
uri: string, uri: string,
claim: ?Claim, claim: ?Claim,
obscureNsfw: boolean, obscureNsfw: boolean,
showUserBlocked: boolean,
claimIsMine: boolean, claimIsMine: boolean,
pending?: boolean, pending?: boolean,
resolveUri: string => void, resolveUri: string => void,
@ -39,6 +41,9 @@ type Props = {
txid: string, txid: string,
nout: number, nout: number,
}>, }>,
blockedChannelUris: Array<string>,
channelIsBlocked: boolean,
isSubscribed: boolean,
}; };
function ClaimPreview(props: Props) { function ClaimPreview(props: Props) {
@ -58,7 +63,11 @@ function ClaimPreview(props: Props) {
type, type,
blackListedOutpoints, blackListedOutpoints,
filteredOutpoints, filteredOutpoints,
blockedChannelUris,
hasVisitedUri, hasVisitedUri,
showUserBlocked,
channelIsBlocked,
isSubscribed,
} = props; } = props;
const haventFetched = claim === undefined; const haventFetched = claim === undefined;
const abandoned = !isResolvingUri && !claim; const abandoned = !isResolvingUri && !claim;
@ -93,6 +102,10 @@ function ClaimPreview(props: Props) {
(outpoint.txid === claim.txid && outpoint.nout === claim.nout) (outpoint.txid === claim.txid && outpoint.nout === claim.nout)
); );
} }
// if showUserBlocked wasnt passed to claimPreview (for blocked page) hide user-blocked channels
if (claim && !shouldHide && !showUserBlocked && blockedChannelUris.length && signingChannel) {
shouldHide = blockedChannelUris.some(blockedUri => blockedUri === signingChannel.permanent_url);
}
function handleContextMenu(e) { function handleContextMenu(e) {
e.preventDefault(); e.preventDefault();
@ -152,7 +165,10 @@ function ClaimPreview(props: Props) {
</div> </div>
{!hideActions && ( {!hideActions && (
<div> <div>
{isChannel && <SubscribeButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />} {isChannel && !channelIsBlocked && (
<SubscribeButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />
)}
{isChannel && !isSubscribed && <BlockButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />}
{!isChannel && <FileProperties uri={uri} />} {!isChannel && <FileProperties uri={uri} />}
</div> </div>
)} )}

View file

@ -128,6 +128,17 @@ export const icons = {
<line x1="12" y1="17" x2="12" y2="17" /> <line x1="12" y1="17" x2="12" y2="17" />
</g> </g>
), ),
[ICONS.BLOCK]: buildIcon(
<g>
<circle cx="12" cy="12" r="10" />
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
</g>
),
[ICONS.UNBLOCK]: buildIcon(
<g>
<circle cx="12" cy="12" r="10" />
</g>
),
[ICONS.LIGHT]: buildIcon( [ICONS.LIGHT]: buildIcon(
<g> <g>
<circle cx="12" cy="12" r="5" /> <circle cx="12" cy="12" r="5" />

View file

@ -21,6 +21,7 @@ import WalletPage from 'page/wallet';
import NavigationHistory from 'page/navigationHistory'; import NavigationHistory from 'page/navigationHistory';
import TagsPage from 'page/tags'; import TagsPage from 'page/tags';
import FollowingPage from 'page/following'; import FollowingPage from 'page/following';
import ListBlocked from 'page/listBlocked';
// Tell the browser we are handling scroll restoration // Tell the browser we are handling scroll restoration
if ('scrollRestoration' in history) { if ('scrollRestoration' in history) {
@ -64,6 +65,7 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} /> <Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} /> <Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} />
<Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<Route path={`/$/${PAGES.BLOCKED}`} exact component={ListBlocked} />
{/* Below need to go at the end to make sure we don't match any of our pages first */} {/* Below need to go at the end to make sure we don't match any of our pages first */}
<Route path="/:claimName" exact component={ShowPage} /> <Route path="/:claimName" exact component={ShowPage} />
<Route path="/:claimName/:contentName" exact component={ShowPage} /> <Route path="/:claimName/:contentName" exact component={ShowPage} />

View file

@ -6,7 +6,7 @@ import { parseURI } from 'lbry-redux';
import Button from 'component/button'; import Button from 'component/button';
import useHover from 'util/use-hover'; import useHover from 'util/use-hover';
type SubscribtionArgs = { type SubscriptionArgs = {
channelName: string, channelName: string,
uri: string, uri: string,
}; };
@ -16,7 +16,7 @@ type Props = {
isSubscribed: boolean, isSubscribed: boolean,
subscriptions: Array<string>, subscriptions: Array<string>,
doChannelSubscribe: ({ channelName: string, uri: string }) => void, doChannelSubscribe: ({ channelName: string, uri: string }) => void,
doChannelUnsubscribe: SubscribtionArgs => void, doChannelUnsubscribe: SubscriptionArgs => void,
doOpenModal: (id: string) => void, doOpenModal: (id: string) => void,
showSnackBarOnSubscribe: boolean, showSnackBarOnSubscribe: boolean,
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,

View file

@ -71,3 +71,5 @@ export const DARK = 'Moon';
export const LIBRARY = 'Folder'; export const LIBRARY = 'Folder';
export const TAG = 'Tag'; export const TAG = 'Tag';
export const SUPPORT = 'TrendingUp'; export const SUPPORT = 'TrendingUp';
export const BLOCK = 'Slash';
export const UNBLOCK = 'Circle';

View file

@ -20,3 +20,4 @@ export const SEARCH = 'search';
export const TRANSACTIONS = 'transactions'; export const TRANSACTIONS = 'transactions';
export const TAGS = 'tags'; export const TAGS = 'tags';
export const WALLET = 'wallet'; export const WALLET = 'wallet';
export const BLOCKED = 'blocked';

View file

@ -6,7 +6,9 @@ import {
makeSelectCoverForUri, makeSelectCoverForUri,
selectCurrentChannelPage, selectCurrentChannelPage,
makeSelectClaimForUri, makeSelectClaimForUri,
selectChannelIsBlocked,
} from 'lbry-redux'; } from 'lbry-redux';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import ChannelPage from './view'; import ChannelPage from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -16,6 +18,8 @@ const select = (state, props) => ({
channelIsMine: makeSelectClaimIsMine(props.uri)(state), channelIsMine: makeSelectClaimIsMine(props.uri)(state),
page: selectCurrentChannelPage(state), page: selectCurrentChannelPage(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
channelIsBlocked: selectChannelIsBlocked(props.uri)(state),
}); });
export default connect( export default connect(

View file

@ -3,6 +3,7 @@ import React, { useState } from 'react';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import Page from 'component/page'; import Page from 'component/page';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import BlockButton from 'component/blockButton';
import ShareButton from 'component/shareButton'; import ShareButton from 'component/shareButton';
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
@ -29,10 +30,24 @@ type Props = {
history: { push: string => void }, history: { push: string => void },
match: { params: { attribute: ?string } }, match: { params: { attribute: ?string } },
channelIsMine: boolean, channelIsMine: boolean,
isSubscribed: boolean,
channelIsBlocked: boolean,
}; };
function ChannelPage(props: Props) { function ChannelPage(props: Props) {
const { uri, title, cover, history, location, page, channelIsMine, thumbnail, claim } = props; const {
uri,
title,
cover,
history,
location,
page,
channelIsMine,
thumbnail,
claim,
isSubscribed,
channelIsBlocked,
} = props;
const { channelName } = parseURI(uri); const { channelName } = parseURI(uri);
const { search } = location; const { search } = location;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
@ -91,7 +106,8 @@ function ChannelPage(props: Props) {
<Tab>{editing ? __('Editing Your Channel') : __('About')}</Tab> <Tab>{editing ? __('Editing Your Channel') : __('About')}</Tab>
<div className="card__actions--inline"> <div className="card__actions--inline">
<ShareButton uri={uri} /> <ShareButton uri={uri} />
<SubscribeButton uri={permanentUrl} /> {!channelIsBlocked && <SubscribeButton uri={permanentUrl} />}
{!isSubscribed && <BlockButton uri={permanentUrl} />}
</div> </div>
</TabList> </TabList>

View file

@ -39,7 +39,7 @@ type Props = {
channelUri: string, channelUri: string,
viewCount: number, viewCount: number,
prepareEdit: ({}, string, {}) => void, prepareEdit: ({}, string, {}) => void,
openModal: (id: string, { uri: string, claimIsMine: boolean, isSupport: boolean }) => void, openModal: (id: string, { [key: string]: any }) => void,
markSubscriptionRead: (string, string) => void, markSubscriptionRead: (string, string) => void,
fetchViewCount: string => void, fetchViewCount: string => void,
balance: number, balance: number,

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { selectBlockedChannels } from 'lbry-redux';
import ListBlocked from './view';
const select = state => ({
uris: selectBlockedChannels(state),
});
export default connect(
select,
null
)(ListBlocked);

View file

@ -0,0 +1,37 @@
// @flow
import React from 'react';
import ClaimList from 'component/claimList';
import Page from 'component/page';
type Props = {
uris: Array<string>,
};
function ListBlocked(props: Props) {
const { uris } = props;
return (
<Page notContained>
{uris && uris.length ? (
<div className="card">
<ClaimList
header={<h1>{__('Your Blocked Channels')}</h1>}
persistedStorageKey="block-list-published"
uris={uris}
defaultSort
/>
</div>
) : (
<div className="main--empty">
<section className="card card--section">
<header className="card__header">
<h2 className="card__title">{__('It looks like you have no blocked channels.')}</h2>
</header>
</section>
</div>
)}
</Page>
);
}
export default ListBlocked;

View file

@ -8,7 +8,7 @@ import {
selectLanguages, selectLanguages,
selectosNotificationsEnabled, selectosNotificationsEnabled,
} from 'redux/selectors/settings'; } from 'redux/selectors/settings';
import { doWalletStatus, selectWalletIsEncrypted } from 'lbry-redux'; import { doWalletStatus, selectWalletIsEncrypted, selectBlockedChannelsCount } from 'lbry-redux';
import SettingsPage from './view'; import SettingsPage from './view';
const select = state => ({ const select = state => ({
@ -26,6 +26,7 @@ const select = state => ({
osNotificationsEnabled: selectosNotificationsEnabled(state), osNotificationsEnabled: selectosNotificationsEnabled(state),
autoDownload: makeSelectClientSetting(settings.AUTO_DOWNLOAD)(state), autoDownload: makeSelectClientSetting(settings.AUTO_DOWNLOAD)(state),
supportOption: makeSelectClientSetting(settings.SUPPORT_OPTION)(state), supportOption: makeSelectClientSetting(settings.SUPPORT_OPTION)(state),
userBlockedChannelsCount: selectBlockedChannelsCount(state),
hideBalance: makeSelectClientSetting(settings.HIDE_BALANCE)(state), hideBalance: makeSelectClientSetting(settings.HIDE_BALANCE)(state),
}); });

View file

@ -1,5 +1,6 @@
// @flow // @flow
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import * as PAGES from 'constants/pages';
import * as React from 'react'; import * as React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { FormField, FormFieldPrice, Form } from 'component/common/form'; import { FormField, FormFieldPrice, Form } from 'component/common/form';
@ -44,6 +45,7 @@ type Props = {
walletEncrypted: boolean, walletEncrypted: boolean,
osNotificationsEnabled: boolean, osNotificationsEnabled: boolean,
supportOption: boolean, supportOption: boolean,
userBlockedChannelsCount?: number,
hideBalance: boolean, hideBalance: boolean,
}; };
@ -153,6 +155,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
setClientSetting, setClientSetting,
supportOption, supportOption,
hideBalance, hideBalance,
userBlockedChannelsCount,
} = this.props; } = this.props;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0; const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
@ -278,6 +281,16 @@ class SettingsPage extends React.PureComponent<Props, State> {
</Form> </Form>
</section> </section>
<section className="card card--section">
<h2 className="card__title">{__('Blocked Channels')}</h2>
<p className="card__subtitle card__help ">
{__('You have')} {userBlockedChannelsCount} {__('blocked')}{' '}
{userBlockedChannelsCount === 1 && __('channel')}
{userBlockedChannelsCount !== 1 && __('channels')}.{' '}
<Button button="link" label={__('Manage')} navigate={`/$/${PAGES.BLOCKED}`} />
</p>
</section>
<section className="card card--section"> <section className="card card--section">
<h2 className="card__title">{__('Notifications')}</h2> <h2 className="card__title">{__('Notifications')}</h2>
<Form> <Form>

View file

@ -8,6 +8,7 @@ import {
notificationsReducer, notificationsReducer,
tagsReducer, tagsReducer,
commentReducer, commentReducer,
blockChannelReducer,
publishReducer, publishReducer,
} from 'lbry-redux'; } from 'lbry-redux';
import { import {
@ -47,6 +48,7 @@ export default history =>
stats: statsReducer, stats: statsReducer,
subscriptions: subscriptionsReducer, subscriptions: subscriptionsReducer,
tags: tagsReducer, tags: tagsReducer,
blockedChannels: blockChannelReducer,
user: userReducer, user: userReducer,
wallet: walletReducer, wallet: walletReducer,
}); });

View file

@ -48,6 +48,7 @@ const appFilter = createFilter('app', ['hasClickedComment', 'searchOptionsExpand
const walletFilter = createFilter('wallet', ['receiveAddress']); const walletFilter = createFilter('wallet', ['receiveAddress']);
const searchFilter = createFilter('search', ['options']); const searchFilter = createFilter('search', ['options']);
const tagsFilter = createFilter('tags', ['followedTags']); const tagsFilter = createFilter('tags', ['followedTags']);
const blockedFilter = createFilter('blockedChannels', ['blockedChannels']);
const whiteListedReducers = [ const whiteListedReducers = [
// @if TARGET='app' // @if TARGET='app'
'publish', 'publish',
@ -59,6 +60,7 @@ const whiteListedReducers = [
'app', 'app',
'search', 'search',
'tags', 'tags',
'blockedChannels',
]; ];
const transforms = [ const transforms = [
@ -66,6 +68,7 @@ const transforms = [
walletFilter, walletFilter,
contentFilter, contentFilter,
fileInfoFilter, fileInfoFilter,
blockedFilter,
// @endif // @endif
appFilter, appFilter,
searchFilter, searchFilter,