New moderation tools: block & mute ()

* initial support for block/mute

* hide blocked + muted content everywhere

* add info message for blocked/muted characteristics

* sort blocked list by most recent block first

* add 'blocked' message on channel page for channels that you have blocked

* cleanup

* delete unused files

* always pass mute/block list to claim_search on homepage

* PR cleanup
This commit is contained in:
Sean Yesmunt 2021-03-03 13:50:16 -05:00 committed by GitHub
parent 277a1d5d1f
commit ea74a66dbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 1115 additions and 877 deletions
flow-typed
package.json
ui
comments.js
component
abandonedChannelPreview
app
blockButton
channelBlockButton
channelContent
channelCreate
channelEdit
channelForm
channelMuteButton
channelStakedIndicator
claimList
claimListDiscover
claimMenuList
claimPreview
claimPreviewTile
claimTilesDiscover
comment
commentMenuList
common
fileAuthor
header
notification
router
constants
modal
modalChannelCreate
modalRemoveBlocked
modalRouter
page
channel
channelNew
channels
channelsFollowingDiscover
listBlocked
settings
redux
scss
store.js
util

View file

@ -27,6 +27,10 @@ declare type CommentsState = {
myReactsByCommentId: any, myReactsByCommentId: any,
othersReactsByCommentId: any, othersReactsByCommentId: any,
pendingCommentReactions: Array<string>, pendingCommentReactions: Array<string>,
moderationBlockList: ?Array<string>,
fetchingModerationBlockList: boolean,
blockingByUri: {},
unBlockingByUri: {},
}; };
declare type CommentReactParams = { declare type CommentReactParams = {

View file

@ -53,7 +53,7 @@
"electron-updater": "^4.2.4", "electron-updater": "^4.2.4",
"express": "^4.17.1", "express": "^4.17.1",
"if-env": "^1.0.4", "if-env": "^1.0.4",
"remove-markdown": "^0.3.0", "remove-markdown": "^0.3.0",
"tempy": "^0.6.0", "tempy": "^0.6.0",
"videojs-logo": "^2.1.4" "videojs-logo": "^2.1.4"
}, },
@ -184,7 +184,7 @@
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"remark": "^9.0.0", "remark": "^9.0.0",
"remark-attr": "^0.8.3", "remark-attr": "^0.8.3",
"remark-breaks": "^1.0.5", "remark-breaks": "^1.0.5",
"remark-emoji": "^2.0.1", "remark-emoji": "^2.0.1",
"remark-frontmatter": "^2.0.0", "remark-frontmatter": "^2.0.0",
"remark-react": "^8.0.0", "remark-react": "^8.0.0",

View file

@ -6,6 +6,8 @@ const Comments = {
enabled: Boolean(COMMENT_SERVER_API), enabled: Boolean(COMMENT_SERVER_API),
moderation_block: (params: ModerationBlockParams) => fetchCommentsApi('moderation.Block', params), moderation_block: (params: ModerationBlockParams) => fetchCommentsApi('moderation.Block', params),
moderation_unblock: (params: ModerationBlockParams) => fetchCommentsApi('moderation.UnBlock', params),
moderation_block_list: (params: ModerationBlockParams) => fetchCommentsApi('moderation.BlockedList', params),
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params), comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params),
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params), comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
}; };
@ -30,8 +32,8 @@ function fetchCommentsApi(method: string, params: {}) {
}; };
return fetch(url, options) return fetch(url, options)
.then(res => res.json()) .then((res) => res.json())
.then(res => res.result); .then((res) => res.result);
} }
export default Comments; export default Comments;

View file

@ -1,14 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectBlockedChannels } from 'redux/selectors/blocked';
import { doChannelUnsubscribe } from 'redux/actions/subscriptions';
import { doOpenModal } from 'redux/actions/app';
import AbandonedChannelPreview from './view'; import AbandonedChannelPreview from './view';
const select = (state, props) => ({ const select = (state, props) => ({});
blockedChannelUris: selectBlockedChannels(state),
});
export default connect(select, { export default connect(select)(AbandonedChannelPreview);
doChannelUnsubscribe,
doOpenModal,
})(AbandonedChannelPreview);

View file

@ -2,28 +2,18 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
import Button from 'component/button';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import * as ICONS from '../../constants/icons'; import ChannelBlockButton from 'component/channelBlockButton';
import * as MODALS from 'constants/modal_types'; import ChannelMuteButton from 'component/channelMuteButton';
type SubscriptionArgs = {
channelName: string,
uri: string,
};
type Props = { type Props = {
uri: string, uri: string,
doChannelUnsubscribe: SubscriptionArgs => void,
type: string, type: string,
blockedChannelUris: Array<string>,
doOpenModal: (string, {}) => void,
}; };
function AbandonedChannelPreview(props: Props) { function AbandonedChannelPreview(props: Props) {
const { uri, doChannelUnsubscribe, type, blockedChannelUris, doOpenModal } = props; const { uri, type } = props;
const { channelName } = parseURI(uri); const { channelName } = parseURI(uri);
const isBlockedChannel = blockedChannelUris.includes(uri);
return ( return (
<li className={classnames('claim-preview__wrapper', 'claim-preview__wrapper--notice')}> <li className={classnames('claim-preview__wrapper', 'claim-preview__wrapper--notice')}>
@ -37,31 +27,10 @@ function AbandonedChannelPreview(props: Props) {
<div className="media__subtitle">{__(`This channel may have been unpublished.`)}</div> <div className="media__subtitle">{__(`This channel may have been unpublished.`)}</div>
</div> </div>
<div className="claim-preview__actions"> <div className="claim-preview__actions">
{isBlockedChannel && ( <div className="section__actions">
<Button <ChannelBlockButton uri={uri} />
iconColor="red" <ChannelMuteButton uri={uri} />
icon={ICONS.UNBLOCK} </div>
button={'alt'}
label={__('Unblock')}
onClick={() => doOpenModal(MODALS.REMOVE_BLOCKED, { blockedUri: uri })}
/>
)}
{/* SubscribeButton uses resolved permanentUri; modifying it didn't seem worth it. */}
{!isBlockedChannel && (
<Button
iconColor="red"
icon={ICONS.UNSUBSCRIBE}
button={'alt'}
label={__('Unfollow')}
onClick={e => {
e.stopPropagation();
doChannelUnsubscribe({
channelName: `@${channelName}`,
uri,
});
}}
/>
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -27,9 +27,10 @@ import {
doSetActiveChannel, doSetActiveChannel,
doSetIncognito, doSetIncognito,
} from 'redux/actions/app'; } from 'redux/actions/app';
import { doFetchModBlockedList } from 'redux/actions/comments';
import App from './view'; import App from './view';
const select = state => ({ const select = (state) => ({
user: selectUser(state), user: selectUser(state),
accessToken: selectAccessToken(state), accessToken: selectAccessToken(state),
theme: selectThemePath(state), theme: selectThemePath(state),
@ -48,18 +49,19 @@ const select = state => ({
myChannelUrls: selectMyChannelUrls(state), myChannelUrls: selectMyChannelUrls(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
fetchAccessToken: () => dispatch(doFetchAccessToken()), fetchAccessToken: () => dispatch(doFetchAccessToken()),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()), fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
setLanguage: language => dispatch(doSetLanguage(language)), setLanguage: (language) => dispatch(doSetLanguage(language)),
signIn: () => dispatch(doSignIn()), signIn: () => dispatch(doSignIn()),
requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()), requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()),
updatePreferences: () => dispatch(doGetAndPopulatePreferences()), updatePreferences: () => dispatch(doGetAndPopulatePreferences()),
getWalletSyncPref: () => dispatch(doGetWalletSyncPreference()), getWalletSyncPref: () => dispatch(doGetWalletSyncPreference()),
syncLoop: noInterval => dispatch(doSyncLoop(noInterval)), syncLoop: (noInterval) => dispatch(doSyncLoop(noInterval)),
setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)), setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)),
setActiveChannelIfNotSet: () => dispatch(doSetActiveChannel()), setActiveChannelIfNotSet: () => dispatch(doSetActiveChannel()),
setIncognito: () => dispatch(doSetIncognito()), setIncognito: () => dispatch(doSetIncognito()),
fetchModBlockedList: () => dispatch(doFetchModBlockedList()),
}); });
export default hot(connect(select, perform)(App)); export default hot(connect(select, perform)(App));

View file

@ -56,14 +56,14 @@ type Props = {
goForward: () => void, goForward: () => void,
index: number, index: number,
length: number, length: number,
push: string => void, push: (string) => void,
}, },
fetchAccessToken: () => void, fetchAccessToken: () => void,
fetchChannelListMine: () => void, fetchChannelListMine: () => void,
signIn: () => void, signIn: () => void,
requestDownloadUpgrade: () => void, requestDownloadUpgrade: () => void,
onSignedIn: () => void, onSignedIn: () => void,
setLanguage: string => void, setLanguage: (string) => void,
isUpgradeAvailable: boolean, isUpgradeAvailable: boolean,
autoUpdateDownloaded: boolean, autoUpdateDownloaded: boolean,
updatePreferences: () => Promise<any>, updatePreferences: () => Promise<any>,
@ -83,7 +83,8 @@ type Props = {
activeChannelClaim: ?ChannelClaim, activeChannelClaim: ?ChannelClaim,
myChannelUrls: ?Array<string>, myChannelUrls: ?Array<string>,
setActiveChannelIfNotSet: () => void, setActiveChannelIfNotSet: () => void,
setIncognito: boolean => void, setIncognito: (boolean) => void,
fetchModBlockedList: () => void,
}; };
function App(props: Props) { function App(props: Props) {
@ -114,6 +115,7 @@ function App(props: Props) {
activeChannelClaim, activeChannelClaim,
setActiveChannelIfNotSet, setActiveChannelIfNotSet,
setIncognito, setIncognito,
fetchModBlockedList,
} = props; } = props;
const appRef = useRef(); const appRef = useRef();
@ -134,7 +136,7 @@ function App(props: Props) {
const showUpgradeButton = const showUpgradeButton =
(autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && !upgradeNagClosed; (autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && !upgradeNagClosed;
// referral claiming // referral claiming
const referredRewardAvailable = rewards && rewards.some(reward => reward.reward_type === REWARDS.TYPE_REFEREE); const referredRewardAvailable = rewards && rewards.some((reward) => reward.reward_type === REWARDS.TYPE_REFEREE);
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const rawReferrerParam = urlParams.get('r'); const rawReferrerParam = urlParams.get('r');
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#'); const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
@ -166,7 +168,7 @@ function App(props: Props) {
useEffect(() => { useEffect(() => {
if (!uploadCount) return; if (!uploadCount) return;
const handleBeforeUnload = event => { const handleBeforeUnload = (event) => {
event.preventDefault(); event.preventDefault();
event.returnValue = 'magic'; // without setting this to something it doesn't work event.returnValue = 'magic'; // without setting this to something it doesn't work
}; };
@ -176,7 +178,7 @@ function App(props: Props) {
// allows user to navigate history using the forward and backward buttons on a mouse // allows user to navigate history using the forward and backward buttons on a mouse
useEffect(() => { useEffect(() => {
const handleForwardAndBackButtons = e => { const handleForwardAndBackButtons = (e) => {
switch (e.button) { switch (e.button) {
case MOUSE_BACK_BTN: case MOUSE_BACK_BTN:
history.index > 0 && history.goBack(); history.index > 0 && history.goBack();
@ -192,7 +194,7 @@ function App(props: Props) {
// allows user to pause miniplayer using the spacebar without the page scrolling down // allows user to pause miniplayer using the spacebar without the page scrolling down
useEffect(() => { useEffect(() => {
const handleKeyPress = e => { const handleKeyPress = (e) => {
if (e.key === ' ' && e.target === document.body) { if (e.key === ' ' && e.target === document.body) {
e.preventDefault(); e.preventDefault();
} }
@ -244,6 +246,10 @@ function App(props: Props) {
} else if (hasNoChannels) { } else if (hasNoChannels) {
setIncognito(true); setIncognito(true);
} }
if (hasMyChannels) {
fetchModBlockedList();
}
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]); }, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]);
useEffect(() => { useEffect(() => {
@ -358,7 +364,7 @@ function App(props: Props) {
[`${MAIN_WRAPPER_CLASS}--scrollbar`]: useCustomScrollbar, [`${MAIN_WRAPPER_CLASS}--scrollbar`]: useCustomScrollbar,
})} })}
ref={appRef} ref={appRef}
onContextMenu={IS_WEB ? undefined : e => openContextMenu(e)} onContextMenu={IS_WEB ? undefined : (e) => openContextMenu(e)}
> >
{IS_WEB && lbryTvApiStatus === STATUS_DOWN ? ( {IS_WEB && lbryTvApiStatus === STATUS_DOWN ? (
<Yrbl <Yrbl

View file

@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import { makeSelectClaimIsMine, makeSelectShortUrlForUri, makeSelectPermanentUrlForUri } from 'lbry-redux';
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
import { doToast } from 'redux/actions/notifications';
import { doToggleBlockChannel } from 'redux/actions/blocked';
import BlockButton from './view';
const select = (state, props) => ({
channelIsBlocked: selectChannelIsBlocked(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
shortUrl: makeSelectShortUrlForUri(props.uri)(state),
permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state),
});
export default connect(select, {
toggleBlockChannel: doToggleBlockChannel,
doToast,
})(BlockButton);

View file

@ -1,49 +0,0 @@
// @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 'effects/use-hover';
type Props = {
permanentUrl: ?string,
shortUrl: string,
isSubscribed: boolean,
toggleBlockChannel: (uri: string) => void,
channelIsBlocked: boolean,
claimIsMine: boolean,
doToast: ({ message: string, linkText: string, linkTarget: string }) => void,
};
export default function BlockButton(props: Props) {
const { permanentUrl, shortUrl, toggleBlockChannel, channelIsBlocked, claimIsMine, doToast } = props;
const blockRef = useRef();
const isHovering = useHover(blockRef);
const blockLabel = channelIsBlocked ? __('Blocked') : __('Block');
const blockTitlePrefix = channelIsBlocked ? __('Unblock this channel') : __('Block this channel');
const blockedOverride = channelIsBlocked && isHovering && __('Unblock');
return permanentUrl && (!claimIsMine || channelIsBlocked) ? (
<Button
ref={blockRef}
icon={ICONS.BLOCK}
button={'alt'}
label={blockedOverride || blockLabel}
title={blockTitlePrefix}
requiresAuth={IS_WEB}
onClick={e => {
e.stopPropagation();
if (!channelIsBlocked) {
doToast({
message: __('Blocked %channelUrl%', { channelUrl: shortUrl }),
linkText: __('Manage'),
linkTarget: `/${PAGES.BLOCKED}`,
});
}
toggleBlockChannel(permanentUrl);
}}
/>
) : null;
}

View file

@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import { doCommentModUnBlock, doCommentModBlock } from 'redux/actions/comments';
import { makeSelectChannelIsBlocked, makeSelectUriIsBlockingOrUnBlocking } from 'redux/selectors/comments';
import ChannelBlockButton from './view';
const select = (state, props) => ({
isBlocked: makeSelectChannelIsBlocked(props.uri)(state),
isBlockingOrUnBlocking: makeSelectUriIsBlockingOrUnBlocking(props.uri)(state),
});
export default connect(select, {
doCommentModUnBlock,
doCommentModBlock,
})(ChannelBlockButton);

View file

@ -0,0 +1,41 @@
// @flow
import React from 'react';
import Button from 'component/button';
type Props = {
uri: string,
isBlocked: boolean,
isBlockingOrUnBlocking: boolean,
doCommentModUnBlock: (string) => void,
doCommentModBlock: (string) => void,
};
function ChannelBlockButton(props: Props) {
const { uri, doCommentModUnBlock, doCommentModBlock, isBlocked, isBlockingOrUnBlocking } = props;
function handleClick() {
if (isBlocked) {
doCommentModUnBlock(uri);
} else {
doCommentModBlock(uri);
}
}
return (
<Button
button={isBlocked ? 'alt' : 'secondary'}
label={
isBlocked
? isBlockingOrUnBlocking
? __('Unblocking...')
: __('Unblock')
: isBlockingOrUnBlocking
? __('Blocking...')
: __('Block')
}
onClick={handleClick}
/>
);
}
export default ChannelBlockButton;

View file

@ -8,7 +8,7 @@ import {
makeSelectClaimForUri, makeSelectClaimForUri,
SETTINGS, SETTINGS,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectChannelIsBlocked } from 'redux/selectors/blocked'; import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
@ -24,7 +24,7 @@ const select = (state, props) => {
fetching: makeSelectFetchingChannelClaims(props.uri)(state), fetching: makeSelectFetchingChannelClaims(props.uri)(state),
totalPages: makeSelectTotalPagesInChannelSearch(props.uri, PAGE_SIZE)(state), totalPages: makeSelectTotalPagesInChannelSearch(props.uri, PAGE_SIZE)(state),
channelIsMine: makeSelectClaimIsMine(props.uri)(state), channelIsMine: makeSelectClaimIsMine(props.uri)(state),
channelIsBlocked: selectChannelIsBlocked(props.uri)(state), channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
claim: props.uri && makeSelectClaimForUri(props.uri)(state), claim: props.uri && makeSelectClaimForUri(props.uri)(state),
isAuthenticated: selectUserVerifiedEmail(state), isAuthenticated: selectUserVerifiedEmail(state),
showMature: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state), showMature: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),

View file

@ -29,6 +29,7 @@ type Props = {
isAuthenticated: boolean, isAuthenticated: boolean,
showMature: boolean, showMature: boolean,
tileLayout: boolean, tileLayout: boolean,
viewBlockedChannel: boolean,
}; };
function ChannelContent(props: Props) { function ChannelContent(props: Props) {
@ -44,6 +45,7 @@ function ChannelContent(props: Props) {
defaultInfiniteScroll = true, defaultInfiniteScroll = true,
showMature, showMature,
tileLayout, tileLayout,
viewBlockedChannel,
} = props; } = props;
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
const [searchQuery, setSearchQuery] = React.useState(''); const [searchQuery, setSearchQuery] = React.useState('');
@ -120,6 +122,7 @@ function ChannelContent(props: Props) {
{claim && claimsInChannel > 0 ? ( {claim && claimsInChannel > 0 ? (
<ClaimListDiscover <ClaimListDiscover
showHiddenByUser={viewBlockedChannel}
forceShowReposts forceShowReposts
tileLayout={tileLayout} tileLayout={tileLayout}
uris={searchResults} uris={searchResults}

View file

@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import ChannelCreate from './view';
import { selectBalance, doCreateChannel, selectCreatingChannel, selectCreateChannelError } from 'lbry-redux';
const select = state => ({
balance: selectBalance(state),
creatingChannel: selectCreatingChannel(state),
createChannelError: selectCreateChannelError(state),
});
const perform = dispatch => ({
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
});
export default connect(select, perform)(ChannelCreate);

View file

@ -1,149 +0,0 @@
// @flow
import React from 'react';
import { isNameValid } from 'lbry-redux';
import { Form, FormField } from 'component/common/form';
import Button from 'component/button';
import analytics from 'analytics';
import LbcSymbol from 'component/common/lbc-symbol';
import { MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR } from 'constants/claim';
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
type Props = {
balance: number,
createChannel: (string, number) => Promise<any>,
onSuccess?: ({}) => void,
creatingChannel: boolean,
createChannelError: ?string,
};
type State = {
newChannelName: string,
newChannelBid: number,
newChannelNameError: string,
newChannelBidError: string,
};
class ChannelCreate extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
newChannelName: '',
newChannelBid: 0.001,
newChannelNameError: '',
newChannelBidError: '',
};
(this: any).handleNewChannelNameChange = this.handleNewChannelNameChange.bind(this);
(this: any).handleNewChannelBidChange = this.handleNewChannelBidChange.bind(this);
(this: any).handleCreateChannel = this.handleCreateChannel.bind(this);
}
handleNewChannelNameChange(event: SyntheticInputEvent<*>) {
let newChannelName = event.target.value;
if (newChannelName.startsWith('@')) {
newChannelName = newChannelName.slice(1);
}
let newChannelNameError;
if (newChannelName.length > 0 && !isNameValid(newChannelName, false)) {
newChannelNameError = INVALID_NAME_ERROR;
}
this.setState({
newChannelNameError,
newChannelName,
});
}
handleNewChannelBidChange(newChannelBid: number) {
const { balance } = this.props;
let newChannelBidError;
if (newChannelBid === 0) {
newChannelBidError = __('Your deposit cannot be 0');
} else if (newChannelBid === balance) {
newChannelBidError = __('Please decrease your deposit to account for transaction fees');
} else if (newChannelBid > balance) {
newChannelBidError = __('Deposit cannot be higher than your available balance');
} else if (newChannelBid < MINIMUM_PUBLISH_BID) {
newChannelBidError = __('Your deposit must be higher');
}
this.setState({
newChannelBid,
newChannelBidError,
});
}
handleCreateChannel() {
const { createChannel, onSuccess } = this.props;
const { newChannelBid, newChannelName } = this.state;
const channelName = `@${newChannelName.trim()}`;
const success = channelClaim => {
analytics.apiLogPublish(channelClaim);
if (onSuccess !== undefined) {
onSuccess({ ...this.props, ...this.state });
}
};
createChannel(channelName, newChannelBid).then(success);
}
render() {
const { newChannelName, newChannelNameError, newChannelBid, newChannelBidError } = this.state;
const { creatingChannel, createChannelError } = this.props;
return (
<Form onSubmit={this.handleCreateChannel}>
{createChannelError && <div className="error__text">{createChannelError}</div>}
<div>
<FormField
label={__('Name')}
name="channel-input"
type="text"
placeholder={__('ChannelName')}
error={newChannelNameError}
value={newChannelName}
onChange={this.handleNewChannelNameChange}
/>
<FormField
className="form-field--price-amount"
name="channel-deposit"
label={<LbcSymbol postfix={__('Deposit')} size={14} />}
step="any"
min="0"
type="number"
helper={
<>
{__(
'These LBRY Credits remain yours. It is a deposit to reserve the name and can be undone at any time.'
)}
<WalletSpendableBalanceHelp inline />
</>
}
error={newChannelBidError}
value={newChannelBid}
onChange={event => this.handleNewChannelBidChange(parseFloat(event.target.value))}
onWheel={e => e.stopPropagation()}
/>
<div className="section__actions">
<Button
type="submit"
button="primary"
label={!creatingChannel ? __('Create channel') : __('Creating channel...')}
disabled={
!newChannelName || !newChannelBid || creatingChannel || newChannelNameError || newChannelBidError
}
/>
</div>
</div>
</Form>
);
}
}
export default ChannelCreate;

View file

@ -16,7 +16,7 @@ import {
doClearChannelErrors, doClearChannelErrors,
} from 'lbry-redux'; } from 'lbry-redux';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { doUpdateBlockListForPublishedChannel } from 'redux/actions/comments';
import ChannelPage from './view'; import ChannelPage from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -38,12 +38,16 @@ const select = (state, props) => ({
balance: selectBalance(state), balance: selectBalance(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
updateChannel: params => dispatch(doUpdateChannel(params)), updateChannel: (params) => dispatch(doUpdateChannel(params)),
createChannel: params => { createChannel: (params) => {
const { name, amount, ...optionalParams } = params; const { name, amount, ...optionalParams } = params;
return dispatch(doCreateChannel('@' + name, amount, optionalParams)); return dispatch(
doCreateChannel('@' + name, amount, optionalParams, (channelClaim) => {
dispatch(doUpdateBlockListForPublishedChannel(channelClaim));
})
);
}, },
clearChannelErrors: () => dispatch(doClearChannelErrors()), clearChannelErrors: () => dispatch(doClearChannelErrors()),
}); });

View file

@ -1,40 +0,0 @@
import { connect } from 'react-redux';
import {
doResolveUri,
selectPublishFormValues,
selectIsStillEditing,
selectMyClaimForUri,
selectIsResolvingPublishUris,
selectTakeOverAmount,
doResetThumbnailStatus,
doClearPublish,
doUpdatePublishForm,
doPrepareEdit,
} from 'lbry-redux';
import { doPublishDesktop } from 'redux/actions/publish';
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
import ChannelForm from './view';
const select = state => ({
...selectPublishFormValues(state),
// The winning claim for a short lbry uri
amountNeededForTakeover: selectTakeOverAmount(state),
// My previously published claims under this short lbry uri
myClaimForUri: selectMyClaimForUri(state),
// If I clicked the "edit" button, have I changed the uri?
// Need this to make it easier to find the source on previously published content
isStillEditing: selectIsStillEditing(state),
isResolvingUri: selectIsResolvingPublishUris(state),
totalRewardValue: selectUnclaimedRewardValue(state),
});
const perform = dispatch => ({
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
clearPublish: () => dispatch(doClearPublish()),
resolveUri: uri => dispatch(doResolveUri(uri)),
publish: filePath => dispatch(doPublishDesktop(filePath)),
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
});
export default connect(select, perform)(ChannelForm);

View file

@ -1,63 +0,0 @@
// @flow
import React, { useEffect, Fragment } from 'react';
import { CHANNEL_NEW } from 'constants/claim';
import { buildURI, isURIValid } from 'lbry-redux';
import ChannelCreate from 'component/channelCreate';
import Card from 'component/common/card';
import * as ICONS from 'constants/icons';
type Props = {
name: ?string,
channel: string,
resolveUri: string => void,
updatePublishForm: any => void,
onSuccess: () => void,
};
function ChannelForm(props: Props) {
const { name, channel, resolveUri, updatePublishForm, onSuccess } = props;
// Every time the channel or name changes, resolve the uris to find winning bid amounts
useEffect(() => {
// If they are midway through a channel creation, treat it as anonymous until it completes
const channelName = channel === CHANNEL_NEW ? '' : channel;
// We are only going to store the full uri, but we need to resolve the uri with and without the channel name
let uri;
try {
uri = name && buildURI({ streamName: name, channelName });
} catch (e) {}
if (channelName && name) {
// resolve without the channel name so we know the winning bid for it
try {
const uriLessChannel = buildURI({ streamName: name });
resolveUri(uriLessChannel);
} catch (e) {}
}
const isValid = isURIValid(uri);
if (uri && isValid) {
resolveUri(uri);
updatePublishForm({ uri });
}
}, [name, channel, resolveUri, updatePublishForm]);
return (
<Fragment>
<Card
icon={ICONS.CHANNEL}
title="Create a New Channel"
subtitle="This is a username or handle that your content can be found under."
actions={
<React.Fragment>
<ChannelCreate onSuccess={onSuccess} onChannelChange={channel => updatePublishForm({ channel })} />
</React.Fragment>
}
/>
</Fragment>
);
}
export default ChannelForm;

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { doToggleMuteChannel } from 'redux/actions/blocked';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import ChannelMuteButton from './view';
const select = (state, props) => ({
isMuted: makeSelectChannelIsMuted(props.uri)(state),
});
export default connect(select, {
doToggleMuteChannel,
})(ChannelMuteButton);

View file

@ -0,0 +1,24 @@
// @flow
import React from 'react';
import Button from 'component/button';
type Props = {
uri: string,
isMuted: boolean,
channelClaim: ?ChannelClaim,
doToggleMuteChannel: (string) => void,
};
function ChannelBlockButton(props: Props) {
const { uri, doToggleMuteChannel, isMuted } = props;
return (
<Button
button={isMuted ? 'alt' : 'secondary'}
label={isMuted ? __('Unmute') : __('Mute')}
onClick={() => doToggleMuteChannel(uri)}
/>
);
}
export default ChannelBlockButton;

View file

@ -11,6 +11,7 @@ import CreditAmount from 'component/common/credit-amount';
type Props = { type Props = {
channelClaim: ChannelClaim, channelClaim: ChannelClaim,
large?: boolean, large?: boolean,
inline?: boolean,
}; };
function getChannelLevel(amount: number): number { function getChannelLevel(amount: number): number {
@ -47,7 +48,7 @@ function getChannelIcon(level: number): string {
} }
function ChannelStakedIndicator(props: Props) { function ChannelStakedIndicator(props: Props) {
const { channelClaim, large = false } = props; const { channelClaim, large = false, inline = false } = props;
if (!channelClaim || !channelClaim.meta) { if (!channelClaim || !channelClaim.meta) {
return null; return null;
@ -79,6 +80,7 @@ function ChannelStakedIndicator(props: Props) {
<div <div
className={classnames('channel-staked__wrapper', { className={classnames('channel-staked__wrapper', {
'channel-staked__wrapper--large': large, 'channel-staked__wrapper--large': large,
'channel-staked__wrapper--inline': inline,
})} })}
> >
<LevelIcon icon={icon} large={large} isControlling={isControlling} /> <LevelIcon icon={icon} large={large} isControlling={isControlling} />

View file

@ -32,12 +32,12 @@ type Props = {
showUnresolvedClaims?: boolean, showUnresolvedClaims?: boolean,
renderProperties: ?(Claim) => Node, renderProperties: ?(Claim) => Node,
includeSupportAction?: boolean, includeSupportAction?: boolean,
hideBlock: boolean,
injectedItem: ?Node, injectedItem: ?Node,
timedOutMessage?: Node, timedOutMessage?: Node,
tileLayout?: boolean, tileLayout?: boolean,
renderActions?: (Claim) => ?Node, renderActions?: (Claim) => ?Node,
searchInLanguage: boolean, searchInLanguage: boolean,
hideMenu?: boolean,
}; };
export default function ClaimList(props: Props) { export default function ClaimList(props: Props) {
@ -57,12 +57,12 @@ export default function ClaimList(props: Props) {
showUnresolvedClaims, showUnresolvedClaims,
renderProperties, renderProperties,
includeSupportAction, includeSupportAction,
hideBlock,
injectedItem, injectedItem,
timedOutMessage, timedOutMessage,
tileLayout = false, tileLayout = false,
renderActions, renderActions,
searchInLanguage, searchInLanguage,
hideMenu,
} = props; } = props;
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW); const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
@ -100,7 +100,8 @@ export default function ClaimList(props: Props) {
return tileLayout && !header ? ( return tileLayout && !header ? (
<section className="claim-grid"> <section className="claim-grid">
{urisLength > 0 && uris.map((uri) => <ClaimPreviewTile key={uri} uri={uri} />)} {urisLength > 0 &&
uris.map((uri) => <ClaimPreviewTile key={uri} uri={uri} showHiddenByUser={showHiddenByUser} />)}
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>} {!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
{timedOut && timedOutMessage && <div className="empty main--empty">{timedOutMessage}</div>} {timedOut && timedOutMessage && <div className="empty main--empty">{timedOutMessage}</div>}
</section> </section>
@ -149,12 +150,13 @@ export default function ClaimList(props: Props) {
<ClaimPreview <ClaimPreview
uri={uri} uri={uri}
type={type} type={type}
hideMenu={hideMenu}
includeSupportAction={includeSupportAction} includeSupportAction={includeSupportAction}
showUnresolvedClaim={showUnresolvedClaims} showUnresolvedClaim={showUnresolvedClaims}
properties={renderProperties || (type !== 'small' ? undefined : false)} properties={renderProperties || (type !== 'small' ? undefined : false)}
renderActions={renderActions} renderActions={renderActions}
showUserBlocked={showHiddenByUser} showUserBlocked={showHiddenByUser}
hideBlock={hideBlock} showHiddenByUser={showHiddenByUser}
customShouldHide={(claim: StreamClaim) => { customShouldHide={(claim: StreamClaim) => {
// Hack to hide spee.ch thumbnail publishes // Hack to hide spee.ch thumbnail publishes
// If it meets these requirements, it was probably uploaded here: // If it meets these requirements, it was probably uploaded here:

View file

@ -7,12 +7,13 @@ import {
SETTINGS, SETTINGS,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectFollowedTags } from 'redux/selectors/tags'; import { selectFollowedTags } from 'redux/selectors/tags';
import { selectBlockedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { doToggleTagFollowDesktop } from 'redux/actions/tags';
import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings';
import { selectModerationBlockList } from 'redux/selectors/comments';
import ClaimListDiscover from './view'; import ClaimListDiscover from './view';
const select = state => ({ const select = (state) => ({
followedTags: selectFollowedTags(state), followedTags: selectFollowedTags(state),
claimSearchByQuery: selectClaimSearchByQuery(state), claimSearchByQuery: selectClaimSearchByQuery(state),
claimSearchByQueryLastPageReached: selectClaimSearchByQueryLastPageReached(state), claimSearchByQueryLastPageReached: selectClaimSearchByQueryLastPageReached(state),
@ -20,7 +21,8 @@ const select = state => ({
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state), showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
languageSetting: selectLanguage(state), languageSetting: selectLanguage(state),
hiddenUris: selectBlockedChannels(state), mutedUris: selectMutedChannels(state),
blockedUris: selectModerationBlockList(state),
searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state), searchInLanguage: makeSelectClientSetting(SETTINGS.SEARCH_IN_LANGUAGE)(state),
}); });

View file

@ -20,17 +20,18 @@ type Props = {
doClaimSearch: ({}) => void, doClaimSearch: ({}) => void,
loading: boolean, loading: boolean,
personalView: boolean, personalView: boolean,
doToggleTagFollowDesktop: string => void, doToggleTagFollowDesktop: (string) => void,
meta?: Node, meta?: Node,
showNsfw: boolean, showNsfw: boolean,
hideReposts: boolean, hideReposts: boolean,
history: { action: string, push: string => void, replace: string => void }, history: { action: string, push: (string) => void, replace: (string) => void },
location: { search: string, pathname: string }, location: { search: string, pathname: string },
claimSearchByQuery: { claimSearchByQuery: {
[string]: Array<string>, [string]: Array<string>,
}, },
claimSearchByQueryLastPageReached: { [string]: boolean }, claimSearchByQueryLastPageReached: { [string]: boolean },
hiddenUris: Array<string>, mutedUris: Array<string>,
blockedUris: Array<string>,
hiddenNsfwMessage?: Node, hiddenNsfwMessage?: Node,
channelIds?: Array<string>, channelIds?: Array<string>,
claimIds?: Array<string>, claimIds?: Array<string>,
@ -43,13 +44,12 @@ type Props = {
header?: Node, header?: Node,
headerLabel?: string | Node, headerLabel?: string | Node,
name?: string, name?: string,
hideBlock?: boolean,
hideAdvancedFilter?: boolean, hideAdvancedFilter?: boolean,
claimType?: Array<string>, claimType?: Array<string>,
defaultClaimType?: Array<string>, defaultClaimType?: Array<string>,
streamType?: string | Array<string>, streamType?: string | Array<string>,
defaultStreamType?: string | Array<string>, defaultStreamType?: string | Array<string>,
renderProperties?: Claim => Node, renderProperties?: (Claim) => Node,
includeSupportAction?: boolean, includeSupportAction?: boolean,
repostedClaimId?: string, repostedClaimId?: string,
pageSize?: number, pageSize?: number,
@ -64,6 +64,7 @@ type Props = {
languageSetting: string, languageSetting: string,
searchInLanguage: boolean, searchInLanguage: boolean,
scrollAnchor?: string, scrollAnchor?: string,
showHiddenByUser?: boolean,
}; };
function ClaimListDiscover(props: Props) { function ClaimListDiscover(props: Props) {
@ -80,7 +81,8 @@ function ClaimListDiscover(props: Props) {
hideReposts, hideReposts,
history, history,
location, location,
hiddenUris, mutedUris,
blockedUris,
hiddenNsfwMessage, hiddenNsfwMessage,
defaultOrderBy, defaultOrderBy,
orderBy, orderBy,
@ -89,7 +91,6 @@ function ClaimListDiscover(props: Props) {
name, name,
claimType, claimType,
pageSize, pageSize,
hideBlock,
defaultClaimType, defaultClaimType,
streamType, streamType,
defaultStreamType, defaultStreamType,
@ -112,6 +113,7 @@ function ClaimListDiscover(props: Props) {
languageSetting, languageSetting,
searchInLanguage, searchInLanguage,
scrollAnchor, scrollAnchor,
showHiddenByUser = false,
} = props; } = props;
const didNavigateForward = history.action === 'PUSH'; const didNavigateForward = history.action === 'PUSH';
const { search } = location; const { search } = location;
@ -120,13 +122,14 @@ function ClaimListDiscover(props: Props) {
const isLargeScreen = useIsLargeScreen(); const isLargeScreen = useIsLargeScreen();
const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING); const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING);
const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING); const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING);
const followed = (followedTags && followedTags.map(t => t.name)) || []; const followed = (followedTags && followedTags.map((t) => t.name)) || [];
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const tagsParam = // can be 'x,y,z' or 'x' or ['x','y'] or CS.CONSTANT const tagsParam = // can be 'x,y,z' or 'x' or ['x','y'] or CS.CONSTANT
(tags && getParamFromTags(tags)) || (tags && getParamFromTags(tags)) ||
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) || (urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
(defaultTags && getParamFromTags(defaultTags)); (defaultTags && getParamFromTags(defaultTags));
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness; const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
const mutedAndBlockedChannelIds = Array.from(new Set(mutedUris.concat(blockedUris).map((uri) => uri.split('#')[1])));
const langParam = urlParams.get(CS.LANGUAGE_KEY) || null; const langParam = urlParams.get(CS.LANGUAGE_KEY) || null;
const languageParams = searchInLanguage const languageParams = searchInLanguage
@ -206,7 +209,7 @@ function ClaimListDiscover(props: Props) {
no_totals: true, no_totals: true,
not_channel_ids: not_channel_ids:
// If channelIdsParam were passed in, we don't need not_channel_ids // If channelIdsParam were passed in, we don't need not_channel_ids
!channelIdsParam && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [], !channelIdsParam ? mutedAndBlockedChannelIds : [],
not_tags: !showNsfw ? MATURE_TAGS : [], not_tags: !showNsfw ? MATURE_TAGS : [],
order_by: order_by:
orderParam === CS.ORDER_BY_TRENDING orderParam === CS.ORDER_BY_TRENDING
@ -247,12 +250,7 @@ function ClaimListDiscover(props: Props) {
if (claimType !== CS.CLAIM_CHANNEL) { if (claimType !== CS.CLAIM_CHANNEL) {
if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) { if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
options.release_time = `>${Math.floor( options.release_time = `>${Math.floor(moment().subtract(1, freshnessParam).startOf('hour').unix())}`;
moment()
.subtract(1, freshnessParam)
.startOf('hour')
.unix()
)}`;
} else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) { } else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
// Warning - hack below // Warning - hack below
// If users are following more than 10 channels or tags, limit results to stuff less than a year old // If users are following more than 10 channels or tags, limit results to stuff less than a year old
@ -263,29 +261,15 @@ function ClaimListDiscover(props: Props) {
(options.channel_ids && options.channel_ids.length > 20) || (options.channel_ids && options.channel_ids.length > 20) ||
(options.any_tags && options.any_tags.length > 20) (options.any_tags && options.any_tags.length > 20)
) { ) {
options.release_time = `>${Math.floor( options.release_time = `>${Math.floor(moment().subtract(3, CS.FRESH_MONTH).startOf('week').unix())}`;
moment()
.subtract(3, CS.FRESH_MONTH)
.startOf('week')
.unix()
)}`;
} else if ( } else if (
(options.channel_ids && options.channel_ids.length > 10) || (options.channel_ids && options.channel_ids.length > 10) ||
(options.any_tags && options.any_tags.length > 10) (options.any_tags && options.any_tags.length > 10)
) { ) {
options.release_time = `>${Math.floor( options.release_time = `>${Math.floor(moment().subtract(1, CS.FRESH_YEAR).startOf('week').unix())}`;
moment()
.subtract(1, CS.FRESH_YEAR)
.startOf('week')
.unix()
)}`;
} else { } else {
// Hack for at least the New page until https://github.com/lbryio/lbry-sdk/issues/2591 is fixed // Hack for at least the New page until https://github.com/lbryio/lbry-sdk/issues/2591 is fixed
options.release_time = `<${Math.floor( options.release_time = `<${Math.floor(moment().startOf('minute').unix())}`;
moment()
.startOf('minute')
.unix()
)}`;
} }
} }
} }
@ -333,14 +317,14 @@ function ClaimListDiscover(props: Props) {
if (hideReposts && !options.reposted_claim_id && !forceShowReposts) { if (hideReposts && !options.reposted_claim_id && !forceShowReposts) {
if (Array.isArray(options.claim_type)) { if (Array.isArray(options.claim_type)) {
if (options.claim_type.length > 1) { if (options.claim_type.length > 1) {
options.claim_type = options.claim_type.filter(claimType => claimType !== 'repost'); options.claim_type = options.claim_type.filter((claimType) => claimType !== 'repost');
} }
} else { } else {
options.claim_type = ['stream', 'channel']; options.claim_type = ['stream', 'channel'];
} }
} }
const hasMatureTags = tagsParam && tagsParam.split(',').some(t => MATURE_TAGS.includes(t)); const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t));
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options); const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
const claimSearchResult = claimSearchByQuery[claimSearchCacheQuery]; const claimSearchResult = claimSearchByQuery[claimSearchCacheQuery];
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery]; const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery];
@ -499,8 +483,8 @@ function ClaimListDiscover(props: Props) {
timedOutMessage={timedOutMessage} timedOutMessage={timedOutMessage}
renderProperties={renderProperties} renderProperties={renderProperties}
includeSupportAction={includeSupportAction} includeSupportAction={includeSupportAction}
hideBlock={hideBlock}
injectedItem={injectedItem} injectedItem={injectedItem}
showHiddenByUser={showHiddenByUser}
/> />
{loading && ( {loading && (
<div className="claim-grid"> <div className="claim-grid">
@ -527,8 +511,8 @@ function ClaimListDiscover(props: Props) {
timedOutMessage={timedOutMessage} timedOutMessage={timedOutMessage}
renderProperties={renderProperties} renderProperties={renderProperties}
includeSupportAction={includeSupportAction} includeSupportAction={includeSupportAction}
hideBlock={hideBlock}
injectedItem={injectedItem} injectedItem={injectedItem}
showHiddenByUser={showHiddenByUser}
/> />
{loading && new Array(dynamicPageSize).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)} {loading && new Array(dynamicPageSize).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
</div> </div>

View file

@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri, makeSelectClaimIsMine } from 'lbry-redux';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { doToggleMuteChannel } from 'redux/actions/blocked';
import { doCommentModBlock, doCommentModUnBlock } from 'redux/actions/comments';
import { makeSelectChannelIsBlocked } from 'redux/selectors/comments';
import ClaimPreview from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
channelIsMuted: makeSelectChannelIsMuted(props.uri)(state),
channelIsBlocked: makeSelectChannelIsBlocked(props.uri)(state),
});
export default connect(select, {
doToggleMuteChannel,
doCommentModBlock,
doCommentModUnBlock,
})(ClaimPreview);

View file

@ -0,0 +1,86 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import classnames from 'classnames';
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
import Icon from 'component/common/icon';
type Props = {
claim: ?Claim,
inline?: boolean,
claimIsMine: boolean,
channelIsMuted: boolean,
channelIsBlocked: boolean,
doToggleMuteChannel: (string) => void,
doCommentModBlock: (string) => void,
doCommentModUnBlock: (string) => void,
};
function ClaimMenuList(props: Props) {
const {
claim,
inline = false,
claimIsMine,
doToggleMuteChannel,
channelIsMuted,
channelIsBlocked,
doCommentModBlock,
doCommentModUnBlock,
} = props;
const channelUri =
claim &&
(claim.value_type === 'channel'
? claim.permanent_url
: claim.signing_channel && claim.signing_channel.permanent_url);
if (!channelUri || claimIsMine) {
return null;
}
function handleToggleMute() {
doToggleMuteChannel(channelUri);
}
function handleToggleBlock() {
if (channelIsBlocked) {
doCommentModUnBlock(channelUri);
} else {
doCommentModBlock(channelUri);
}
}
return (
<Menu>
<MenuButton
className={classnames('menu__button', { 'claim__menu-button': !inline, 'claim__menu-button--inline': inline })}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<Icon size={20} icon={ICONS.MORE_VERTICAL} />
</MenuButton>
<MenuList className="menu__list">
{!claimIsMine && (
<>
<MenuItem className="comment__menu-option" onSelect={handleToggleBlock}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.BLOCK} />
{channelIsBlocked ? __('Unblock Channel') : __('Block Channel')}
</div>
</MenuItem>
<MenuItem className="comment__menu-option" onSelect={handleToggleMute}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.MUTE} />
{channelIsMuted ? __('Unmute Channel') : __('Mute Channel')}
</div>
</MenuItem>
</>
)}
</MenuList>
</Menu>
);
}
export default ClaimMenuList;

View file

@ -11,11 +11,12 @@ import {
makeSelectClaimWasPurchased, makeSelectClaimWasPurchased,
makeSelectStreamingUrlForUri, makeSelectStreamingUrlForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectBlockedChannels, selectChannelIsBlocked } from 'redux/selectors/blocked'; import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content'; import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { selectModerationBlockList } from 'redux/selectors/comments';
import ClaimPreview from './view'; import ClaimPreview from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -30,17 +31,18 @@ const select = (state, props) => ({
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state), nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state), blackListedOutpoints: selectBlackListedOutpoints(state),
filteredOutpoints: selectFilteredOutpoints(state), filteredOutpoints: selectFilteredOutpoints(state),
blockedChannelUris: selectBlockedChannels(state), mutedUris: selectMutedChannels(state),
blockedUris: selectModerationBlockList(state),
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state), hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state), channelIsBlocked: props.uri && makeSelectChannelIsMuted(props.uri)(state),
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state), isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state), streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state), wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: (uri) => dispatch(doResolveUri(uri)),
getFile: uri => dispatch(doFileGet(uri, false)), getFile: (uri) => dispatch(doFileGet(uri, false)),
}); });
export default connect(select, perform)(ClaimPreview); export default connect(select, perform)(ClaimPreview);

View file

@ -3,7 +3,6 @@ import type { Node } from 'react';
import React, { useEffect, forwardRef } from 'react'; import React, { useEffect, forwardRef } from 'react';
import { NavLink, withRouter } from 'react-router-dom'; import { NavLink, withRouter } from 'react-router-dom';
import classnames from 'classnames'; import classnames from 'classnames';
import { SIMPLE_SITE } from 'config';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
// @if TARGET='app' // @if TARGET='app'
import { openClaimPreviewMenu } from 'util/context-menu'; import { openClaimPreviewMenu } from 'util/context-menu';
@ -16,7 +15,6 @@ 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 ClaimSupportButton from 'component/claimSupportButton'; import ClaimSupportButton from 'component/claimSupportButton';
import useGetThumbnail from 'effects/use-get-thumbnail'; import useGetThumbnail from 'effects/use-get-thumbnail';
import ClaimPreviewTitle from 'component/claimPreviewTitle'; import ClaimPreviewTitle from 'component/claimPreviewTitle';
@ -25,6 +23,7 @@ import ClaimRepostAuthor from 'component/claimRepostAuthor';
import FileDownloadLink from 'component/fileDownloadLink'; import FileDownloadLink from 'component/fileDownloadLink';
import AbandonedChannelPreview from 'component/abandonedChannelPreview'; import AbandonedChannelPreview from 'component/abandonedChannelPreview';
import PublishPending from 'component/publishPending'; import PublishPending from 'component/publishPending';
import ClaimMenuList from 'component/claimMenuList';
import ClaimPreviewLoading from './claim-preview-loading'; import ClaimPreviewLoading from './claim-preview-loading';
import ClaimPreviewHidden from './claim-preview-no-mature'; import ClaimPreviewHidden from './claim-preview-no-mature';
import ClaimPreviewNoContent from './claim-preview-no-content'; import ClaimPreviewNoContent from './claim-preview-no-content';
@ -53,14 +52,13 @@ type Props = {
txid: string, txid: string,
nout: number, nout: number,
}>, }>,
blockedChannelUris: Array<string>, mutedUris: Array<string>,
blockedUris: Array<string>,
channelIsBlocked: boolean, channelIsBlocked: boolean,
isSubscribed: boolean,
actions: boolean | Node | string | number, actions: boolean | Node | string | number,
properties: boolean | Node | string | number | ((Claim) => Node), properties: boolean | Node | string | number | ((Claim) => Node),
empty?: Node, empty?: Node,
onClick?: (any) => any, onClick?: (any) => any,
hideBlock?: boolean,
streamingUrl: ?string, streamingUrl: ?string,
getFile: (string) => void, getFile: (string) => void,
customShouldHide?: (Claim) => boolean, customShouldHide?: (Claim) => boolean,
@ -72,6 +70,7 @@ type Props = {
wrapperElement?: string, wrapperElement?: string,
hideRepostLabel?: boolean, hideRepostLabel?: boolean,
repostUrl?: string, repostUrl?: string,
hideMenu?: boolean,
}; };
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => { const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
@ -87,7 +86,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
// is the claim consider nsfw? // is the claim consider nsfw?
nsfw, nsfw,
claimIsMine, claimIsMine,
isSubscribed,
streamingUrl, streamingUrl,
// user properties // user properties
channelIsBlocked, channelIsBlocked,
@ -113,13 +111,14 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
hideActions = false, hideActions = false,
properties, properties,
onClick, onClick,
hideBlock,
actions, actions,
blockedChannelUris, mutedUris,
blockedUris,
blackListedOutpoints, blackListedOutpoints,
filteredOutpoints, filteredOutpoints,
includeSupportAction, includeSupportAction,
renderActions, renderActions,
hideMenu = false,
// repostUrl, // repostUrl,
} = props; } = props;
const WrapperElement = wrapperElement || 'li'; const WrapperElement = wrapperElement || 'li';
@ -172,13 +171,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
); );
} }
// block stream claims // block stream claims
if (claim && !shouldHide && !showUserBlocked && blockedChannelUris.length && signingChannel) { if (claim && !shouldHide && !showUserBlocked && mutedUris.length && signingChannel) {
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === signingChannel.permanent_url); shouldHide = mutedUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
} }
// block channel claims if we can't control for them in claim search if (claim && !shouldHide && !showUserBlocked && blockedUris.length && signingChannel) {
// e.g. fetchRecommendedSubscriptions shouldHide = blockedUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
if (claim && isChannelUri && !shouldHide && !showUserBlocked && blockedChannelUris.length) {
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === claim.permanent_url);
} }
if (!shouldHide && customShouldHide && claim) { if (!shouldHide && customShouldHide && claim) {
@ -275,7 +272,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
> >
{isChannelUri && claim ? ( {isChannelUri && claim ? (
<UriIndicator uri={contentUri} link> <UriIndicator uri={contentUri} link>
<ChannelThumbnail uri={contentUri} obscure={channelIsBlocked} /> <ChannelThumbnail uri={contentUri} />
</UriIndicator> </UriIndicator>
) : ( ) : (
<> <>
@ -313,9 +310,9 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
</NavLink> </NavLink>
)} )}
{type !== 'small' && !isChannelUri && signingChannel && SIMPLE_SITE && ( {/* {type !== 'small' && !isChannelUri && signingChannel && SIMPLE_SITE && (
<ChannelThumbnail uri={signingChannel.permanent_url} /> <ChannelThumbnail uri={signingChannel.permanent_url} />
)} )} */}
</div> </div>
<ClaimPreviewSubtitle uri={uri} type={type} /> <ClaimPreviewSubtitle uri={uri} type={type} />
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />} {(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
@ -329,12 +326,16 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
actions actions
) : ( ) : (
<div className="claim-preview__primary-actions"> <div className="claim-preview__primary-actions">
{!isChannelUri && signingChannel && (
<div className="claim-preview__channel-staked">
<ChannelThumbnail uri={signingChannel.permanent_url} />
</div>
)}
{isChannelUri && !channelIsBlocked && !claimIsMine && ( {isChannelUri && !channelIsBlocked && !claimIsMine && (
<SubscribeButton uri={contentUri.startsWith('lbry://') ? contentUri : `lbry://${contentUri}`} /> <SubscribeButton uri={contentUri.startsWith('lbry://') ? contentUri : `lbry://${contentUri}`} />
)} )}
{!hideBlock && isChannelUri && !isSubscribed && (!claimIsMine || channelIsBlocked) && (
<BlockButton uri={contentUri.startsWith('lbry://') ? contentUri : `lbry://${contentUri}`} />
)}
{includeSupportAction && <ClaimSupportButton uri={uri} />} {includeSupportAction && <ClaimSupportButton uri={uri} />}
</div> </div>
)} )}
@ -355,6 +356,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
)} )}
</div> </div>
</div> </div>
{!hideMenu && <ClaimMenuList uri={uri} />}
</WrapperElement> </WrapperElement>
); );
}); });

View file

@ -9,7 +9,7 @@ import {
makeSelectChannelForClaimUri, makeSelectChannelForClaimUri,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectBlockedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import ClaimPreviewTile from './view'; import ClaimPreviewTile from './view';
@ -22,14 +22,14 @@ const select = (state, props) => ({
title: props.uri && makeSelectTitleForUri(props.uri)(state), title: props.uri && makeSelectTitleForUri(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state), blackListedOutpoints: selectBlackListedOutpoints(state),
filteredOutpoints: selectFilteredOutpoints(state), filteredOutpoints: selectFilteredOutpoints(state),
blockedChannelUris: selectBlockedChannels(state), blockedChannelUris: selectMutedChannels(state),
showMature: selectShowMatureContent(state), showMature: selectShowMatureContent(state),
isMature: makeSelectClaimIsNsfw(props.uri)(state), isMature: makeSelectClaimIsNsfw(props.uri)(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: (uri) => dispatch(doResolveUri(uri)),
getFile: uri => dispatch(doFileGet(uri, false)), getFile: (uri) => dispatch(doFileGet(uri, false)),
}); });
export default connect(select, perform)(ClaimPreviewTile); export default connect(select, perform)(ClaimPreviewTile);

View file

@ -14,6 +14,8 @@ import { parseURI } from 'lbry-redux';
import FileProperties from 'component/fileProperties'; import FileProperties from 'component/fileProperties';
import FileDownloadLink from 'component/fileDownloadLink'; import FileDownloadLink from 'component/fileDownloadLink';
import ClaimRepostAuthor from 'component/claimRepostAuthor'; import ClaimRepostAuthor from 'component/claimRepostAuthor';
import ClaimMenuList from 'component/claimMenuList';
// @if TARGET='app' // @if TARGET='app'
import { openClaimPreviewMenu } from 'util/context-menu'; import { openClaimPreviewMenu } from 'util/context-menu';
// @endif // @endif
@ -41,6 +43,7 @@ type Props = {
streamingUrl: string, streamingUrl: string,
isMature: boolean, isMature: boolean,
showMature: boolean, showMature: boolean,
showHiddenByUser?: boolean,
}; };
function ClaimPreviewTile(props: Props) { function ClaimPreviewTile(props: Props) {
@ -60,6 +63,7 @@ function ClaimPreviewTile(props: Props) {
blockedChannelUris, blockedChannelUris,
isMature, isMature,
showMature, showMature,
showHiddenByUser,
} = props; } = props;
const isRepost = claim && claim.repost_channel_url; const isRepost = claim && claim.repost_channel_url;
const shouldFetch = claim === undefined; const shouldFetch = claim === undefined;
@ -128,12 +132,12 @@ function ClaimPreviewTile(props: Props) {
} }
// block stream claims // block stream claims
if (claim && !shouldHide && blockedChannelUris.length && signingChannel) { if (claim && !shouldHide && !showHiddenByUser && blockedChannelUris.length && signingChannel) {
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === signingChannel.permanent_url); shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
} }
// block channel claims if we can't control for them in claim search // block channel claims if we can't control for them in claim search
// e.g. fetchRecommendedSubscriptions // e.g. fetchRecommendedSubscriptions
if (claim && isChannel && !shouldHide && blockedChannelUris.length) { if (claim && isChannel && !shouldHide && !showHiddenByUser && blockedChannelUris.length) {
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === claim.permanent_url); shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === claim.permanent_url);
} }
@ -194,6 +198,7 @@ function ClaimPreviewTile(props: Props) {
<UriIndicator uri={uri} link /> <UriIndicator uri={uri} link />
</div> </div>
)} )}
<ClaimMenuList uri={uri} />
</h2> </h2>
</NavLink> </NavLink>
<div> <div>

View file

@ -1,16 +1,18 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doClaimSearch, selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, SETTINGS } from 'lbry-redux'; import { doClaimSearch, selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, SETTINGS } from 'lbry-redux';
import { selectBlockedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectModerationBlockList } from 'redux/selectors/comments';
import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { doToggleTagFollowDesktop } from 'redux/actions/tags';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import ClaimListDiscover from './view'; import ClaimListDiscover from './view';
const select = state => ({ const select = (state) => ({
claimSearchByQuery: selectClaimSearchByQuery(state), claimSearchByQuery: selectClaimSearchByQuery(state),
fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state), fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state), showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
hiddenUris: selectBlockedChannels(state), mutedUris: selectMutedChannels(state),
blockedUris: selectModerationBlockList(state),
}); });
const perform = { const perform = {

View file

@ -10,7 +10,7 @@ type Props = {
doClaimSearch: ({}) => void, doClaimSearch: ({}) => void,
showNsfw: boolean, showNsfw: boolean,
hideReposts: boolean, hideReposts: boolean,
history: { action: string, push: string => void, replace: string => void }, history: { action: string, push: (string) => void, replace: (string) => void },
claimSearchByQuery: { claimSearchByQuery: {
[string]: Array<string>, [string]: Array<string>,
}, },
@ -19,10 +19,10 @@ type Props = {
}, },
// claim search options are below // claim search options are below
tags: Array<string>, tags: Array<string>,
hiddenUris: Array<string>, blockedUris: Array<string>,
mutedUris: Array<string>,
claimIds?: Array<string>, claimIds?: Array<string>,
channelIds?: Array<string>, channelIds?: Array<string>,
notChannelIds?: Array<string>,
pageSize: number, pageSize: number,
orderBy?: Array<string>, orderBy?: Array<string>,
releaseTime?: string, releaseTime?: string,
@ -39,12 +39,12 @@ function ClaimTilesDiscover(props: Props) {
claimSearchByQuery, claimSearchByQuery,
showNsfw, showNsfw,
hideReposts, hideReposts,
hiddenUris, blockedUris,
mutedUris,
// Below are options to pass that are forwarded to claim_search // Below are options to pass that are forwarded to claim_search
tags, tags,
channelIds, channelIds,
claimIds, claimIds,
notChannelIds,
orderBy, orderBy,
pageSize = 8, pageSize = 8,
releaseTime, releaseTime,
@ -60,6 +60,7 @@ function ClaimTilesDiscover(props: Props) {
const urlParams = new URLSearchParams(location.search); const urlParams = new URLSearchParams(location.search);
const feeAmountInUrl = urlParams.get('fee_amount'); const feeAmountInUrl = urlParams.get('fee_amount');
const feeAmountParam = feeAmountInUrl || feeAmount; const feeAmountParam = feeAmountInUrl || feeAmount;
const mutedAndBlockedChannelIds = Array.from(new Set(mutedUris.concat(blockedUris).map((uri) => uri.split('#')[1])));
const options: { const options: {
page_size: number, page_size: number,
no_totals: boolean, no_totals: boolean,
@ -86,10 +87,7 @@ function ClaimTilesDiscover(props: Props) {
not_tags: !showNsfw ? MATURE_TAGS : [], not_tags: !showNsfw ? MATURE_TAGS : [],
any_languages: languages, any_languages: languages,
channel_ids: channelIds || [], channel_ids: channelIds || [],
not_channel_ids: not_channel_ids: mutedAndBlockedChannelIds || [],
notChannelIds ||
// If channelIds were passed in, we don't need not_channel_ids
(!channelIds && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : []),
order_by: orderBy || ['trending_group', 'trending_mixed'], order_by: orderBy || ['trending_group', 'trending_mixed'],
}; };
@ -108,7 +106,7 @@ function ClaimTilesDiscover(props: Props) {
// https://github.com/lbryio/lbry-desktop/issues/3774 // https://github.com/lbryio/lbry-desktop/issues/3774
if (hideReposts) { if (hideReposts) {
if (Array.isArray(options.claim_type)) { if (Array.isArray(options.claim_type)) {
options.claim_type = options.claim_type.filter(claimType => claimType !== 'repost'); options.claim_type = options.claim_type.filter((claimType) => claimType !== 'repost');
} else { } else {
options.claim_type = ['stream', 'channel']; options.claim_type = ['stream', 'channel'];
} }
@ -143,7 +141,7 @@ function ClaimTilesDiscover(props: Props) {
return ( return (
<ul className="claim-grid"> <ul className="claim-grid">
{uris && uris.length {uris && uris.length
? uris.map(uri => <ClaimPreviewTile key={uri} uri={uri} />) ? uris.map((uri) => <ClaimPreviewTile key={uri} uri={uri} />)
: new Array(pageSize).fill(1).map((x, i) => <ClaimPreviewTile key={i} placeholder />)} : new Array(pageSize).fill(1).map((x, i) => <ClaimPreviewTile key={i} placeholder />)}
</ul> </ul>
); );

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectThumbnailForUri, selectMyChannelClaims } from 'lbry-redux'; import { makeSelectThumbnailForUri, selectMyChannelClaims } from 'lbry-redux';
import { doCommentUpdate } from 'redux/actions/comments'; import { doCommentUpdate } from 'redux/actions/comments';
import { selectChannelIsBlocked } from 'redux/selectors/blocked'; import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { doSetPlayingUri } from 'redux/actions/content'; import { doSetPlayingUri } from 'redux/actions/content';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
@ -11,7 +11,7 @@ import Comment from './view';
const select = (state, props) => ({ const select = (state, props) => ({
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state), thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
channelIsBlocked: props.authorUri && selectChannelIsBlocked(props.authorUri)(state), channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state), othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
activeChannelClaim: selectActiveChannelClaim(state), activeChannelClaim: selectActiveChannelClaim(state),

View file

@ -200,7 +200,7 @@ function Comment(props: Props) {
</div> </div>
<div className="comment__menu"> <div className="comment__menu">
<Menu> <Menu>
<MenuButton> <MenuButton className="menu__button">
<Icon <Icon
size={18} size={18}
className={mouseIsHovering ? 'comment__menu-icon--hovering' : 'comment__menu-icon'} className={mouseIsHovering ? 'comment__menu-icon--hovering' : 'comment__menu-icon'}

View file

@ -1,12 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux'; import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
import { import { doCommentAbandon, doCommentPin, doCommentList, doCommentModBlock } from 'redux/actions/comments';
doCommentAbandon, import { doToggleMuteChannel } from 'redux/actions/blocked';
doCommentPin,
doCommentList,
// doCommentModBlock,
} from 'redux/actions/comments';
import { doToggleBlockChannel } from 'redux/actions/blocked';
// import { doSetActiveChannel } from 'redux/actions/app'; // import { doSetActiveChannel } from 'redux/actions/app';
import { doSetPlayingUri } from 'redux/actions/content'; import { doSetPlayingUri } from 'redux/actions/content';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
@ -19,14 +14,14 @@ const select = (state, props) => ({
activeChannelClaim: selectActiveChannelClaim(state), activeChannelClaim: selectActiveChannelClaim(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
closeInlinePlayer: () => dispatch(doSetPlayingUri({ uri: null })), closeInlinePlayer: () => dispatch(doSetPlayingUri({ uri: null })),
deleteComment: (commentId, creatorChannelUrl) => dispatch(doCommentAbandon(commentId, creatorChannelUrl)), deleteComment: (commentId, creatorChannelUrl) => dispatch(doCommentAbandon(commentId, creatorChannelUrl)),
blockChannel: channelUri => dispatch(doToggleBlockChannel(channelUri)), blockChannel: (channelUri) => dispatch(doToggleMuteChannel(channelUri)),
pinComment: (commentId, remove) => dispatch(doCommentPin(commentId, remove)), pinComment: (commentId, remove) => dispatch(doCommentPin(commentId, remove)),
fetchComments: uri => dispatch(doCommentList(uri)), fetchComments: (uri) => dispatch(doCommentList(uri)),
// setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)), // setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)),
// commentModBlock: commentAuthor => dispatch(doCommentModBlock(commentAuthor)), commentModBlock: (commentAuthor) => dispatch(doCommentModBlock(commentAuthor)),
}); });
export default connect(select, perform)(CommentMenuList); export default connect(select, perform)(CommentMenuList);

View file

@ -23,7 +23,7 @@ type Props = {
activeChannelClaim: ?ChannelClaim, activeChannelClaim: ?ChannelClaim,
claimIsMine: boolean, claimIsMine: boolean,
isTopLevel: boolean, isTopLevel: boolean,
// commentModBlock: string => void, commentModBlock: (string) => void,
}; };
function CommentMenuList(props: Props) { function CommentMenuList(props: Props) {
@ -43,17 +43,10 @@ function CommentMenuList(props: Props) {
isPinned, isPinned,
handleEditComment, handleEditComment,
fetchComments, fetchComments,
// commentModBlock, commentModBlock,
// setActiveChannel,
} = props; } = props;
const activeChannelIsCreator = activeChannelClaim && activeChannelClaim.permanent_url === contentChannelPermanentUrl; const activeChannelIsCreator = activeChannelClaim && activeChannelClaim.permanent_url === contentChannelPermanentUrl;
// let authorChannel;
// try {
// const { claimName } = parseURI(authorUri);
// authorChannel = claimName;
// } catch (e) {}
function handlePinComment(commentId, remove) { function handlePinComment(commentId, remove) {
pinComment(commentId, remove).then(() => fetchComments(uri)); pinComment(commentId, remove).then(() => fetchComments(uri));
} }
@ -65,32 +58,16 @@ function CommentMenuList(props: Props) {
function handleCommentBlock() { function handleCommentBlock() {
if (claimIsMine) { if (claimIsMine) {
// Block them from commenting on future content commentModBlock(authorUri);
// commentModBlock(authorUri);
} }
}
function handleCommentMute() {
blockChannel(authorUri); blockChannel(authorUri);
} }
// function handleChooseChannel() {
// const { channelClaimId } = parseURI(authorUri);
// setActiveChannel(channelClaimId);
// }
return ( return (
<MenuList className="menu__list--comments"> <MenuList className="menu__list">
{/* {commentIsMine && activeChannelClaim && activeChannelClaim.permanent_url !== authorUri && (
<MenuItem className="comment__menu-option" onSelect={handleChooseChannel}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('Use this channel')}
</div>
<span className="comment__menu-help">
{__('Switch to %channel_name% to interact with this comment', { channel_name: authorChannel })}.
</span>
</MenuItem>
)} */}
{activeChannelIsCreator && <div className="comment__menu-title">{__('Creator tools')}</div>} {activeChannelIsCreator && <div className="comment__menu-title">{__('Creator tools')}</div>}
{activeChannelIsCreator && isTopLevel && ( {activeChannelIsCreator && isTopLevel && (
@ -123,16 +100,27 @@ function CommentMenuList(props: Props) {
</MenuItem> </MenuItem>
)} )}
{/* Disabled until we deal with current app blocklist parity */}
{!commentIsMine && ( {!commentIsMine && (
<MenuItem className="comment__menu-option" onSelect={handleCommentBlock}> <MenuItem className="comment__menu-option" onSelect={handleCommentBlock}>
<div className="menu__link"> <div className="menu__link">
<Icon aria-hidden icon={ICONS.BLOCK} /> <Icon aria-hidden icon={ICONS.BLOCK} />
{__('Block')} {__('Block')}
</div> </div>
{/* {activeChannelIsCreator && ( {activeChannelIsCreator && (
<span className="comment__menu-help">Hide this channel's comments and block them from commenting.</span> <span className="comment__menu-help">{__('Prevent this channel from interacting with you.')}</span>
)} */} )}
</MenuItem>
)}
{!commentIsMine && (
<MenuItem className="comment__menu-option" onSelect={handleCommentMute}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.MUTE} />
{__('Mute')}
</div>
{activeChannelIsCreator && (
<span className="comment__menu-help">{__('Hide this channel for you only.')}</span>
)}
</MenuItem> </MenuItem>
)} )}

View file

@ -238,6 +238,13 @@ export const icons = {
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
</g> </g>
), ),
[ICONS.MUTE]: buildIcon(
<g>
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
<line x1="23" y1="9" x2="17" y2="15" />
<line x1="17" y1="9" x2="23" y2="15" />
</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

@ -10,7 +10,7 @@ function FileAuthor(props: Props) {
const { channelUri } = props; const { channelUri } = props;
return channelUri ? ( return channelUri ? (
<ClaimPreview uri={channelUri} type="inline" properties={false} hideBlock /> <ClaimPreview uri={channelUri} type="inline" properties={false} />
) : ( ) : (
<div className="claim-preview--inline claim-preview__title">{__('Anonymous')}</div> <div className="claim-preview--inline claim-preview__title">{__('Anonymous')}</div>
); );

View file

@ -10,7 +10,7 @@ import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/setting
import { selectHasNavigated, selectActiveChannelClaim } from 'redux/selectors/app'; import { selectHasNavigated, selectActiveChannelClaim } from 'redux/selectors/app';
import Header from './view'; import Header from './view';
const select = state => ({ const select = (state) => ({
language: selectLanguage(state), language: selectLanguage(state),
balance: selectBalance(state), balance: selectBalance(state),
roundedSpendableBalance: formatCredits(selectBalance(state), 2, true), roundedSpendableBalance: formatCredits(selectBalance(state), 2, true),
@ -27,10 +27,9 @@ const select = state => ({
activeChannelClaim: selectActiveChannelClaim(state), activeChannelClaim: selectActiveChannelClaim(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
setClientSetting: (key, value, push) => dispatch(doSetClientSetting(key, value, push)), setClientSetting: (key, value, push) => dispatch(doSetClientSetting(key, value, push)),
signOut: () => dispatch(doSignOut()), signOut: () => dispatch(doSignOut()),
openChannelCreate: () => dispatch(doOpenModal(MODALS.CREATE_CHANNEL)),
openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)), openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)),
clearEmailEntry: () => dispatch(doClearEmailEntry()), clearEmailEntry: () => dispatch(doClearEmailEntry()),
clearPasswordEntry: () => dispatch(doClearPasswordEntry()), clearPasswordEntry: () => dispatch(doClearPasswordEntry()),

View file

@ -33,8 +33,8 @@ type Props = {
index: number, index: number,
length: number, length: number,
location: { pathname: string }, location: { pathname: string },
push: string => void, push: (string) => void,
replace: string => void, replace: (string) => void,
}, },
currentTheme: string, currentTheme: string,
automaticDarkModeEnabled: boolean, automaticDarkModeEnabled: boolean,
@ -52,13 +52,12 @@ type Props = {
syncError: ?string, syncError: ?string,
emailToVerify?: string, emailToVerify?: string,
signOut: () => void, signOut: () => void,
openChannelCreate: () => void,
openSignOutModal: () => void, openSignOutModal: () => void,
clearEmailEntry: () => void, clearEmailEntry: () => void,
clearPasswordEntry: () => void, clearPasswordEntry: () => void,
hasNavigated: boolean, hasNavigated: boolean,
sidebarOpen: boolean, sidebarOpen: boolean,
setSidebarOpen: boolean => void, setSidebarOpen: (boolean) => void,
isAbsoluteSideNavHidden: boolean, isAbsoluteSideNavHidden: boolean,
hideCancel: boolean, hideCancel: boolean,
activeChannelClaim: ?ChannelClaim, activeChannelClaim: ?ChannelClaim,
@ -181,7 +180,7 @@ const Header = (props: Props) => {
label={hideBalance || Number(roundedBalance) === 0 ? __('Your Wallet') : roundedBalance} label={hideBalance || Number(roundedBalance) === 0 ? __('Your Wallet') : roundedBalance}
icon={ICONS.LBC} icon={ICONS.LBC}
// @if TARGET='app' // @if TARGET='app'
onDoubleClick={e => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
// @endif // @endif
@ -197,7 +196,7 @@ const Header = (props: Props) => {
// @endif // @endif
})} })}
// @if TARGET='app' // @if TARGET='app'
onDoubleClick={e => { onDoubleClick={(e) => {
remote.getCurrentWindow().maximize(); remote.getCurrentWindow().maximize();
}} }}
// @endif // @endif
@ -250,7 +249,7 @@ const Header = (props: Props) => {
if (history.location.pathname === '/') window.location.reload(); if (history.location.pathname === '/') window.location.reload();
}} }}
// @if TARGET='app' // @if TARGET='app'
onDoubleClick={e => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
// @endif // @endif
@ -308,7 +307,7 @@ const Header = (props: Props) => {
icon={ICONS.REMOVE} icon={ICONS.REMOVE}
{...closeButtonNavigationProps} {...closeButtonNavigationProps}
// @if TARGET='app' // @if TARGET='app'
onDoubleClick={e => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
// @endif // @endif
@ -326,8 +325,8 @@ const Header = (props: Props) => {
type HeaderMenuButtonProps = { type HeaderMenuButtonProps = {
authenticated: boolean, authenticated: boolean,
notificationsEnabled: boolean, notificationsEnabled: boolean,
history: { push: string => void }, history: { push: (string) => void },
handleThemeToggle: string => void, handleThemeToggle: (string) => void,
currentTheme: string, currentTheme: string,
activeChannelUrl: ?string, activeChannelUrl: ?string,
openSignOutModal: () => void, openSignOutModal: () => void,
@ -357,7 +356,7 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
title={__('Publish a file, or create a channel')} title={__('Publish a file, or create a channel')}
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden" className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
// @if TARGET='app' // @if TARGET='app'
onDoubleClick={e => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
// @endif // @endif
@ -386,7 +385,7 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
title={__('Settings')} title={__('Settings')}
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden" className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
// @if TARGET='app' // @if TARGET='app'
onDoubleClick={e => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
// @endif // @endif
@ -419,7 +418,7 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
'header__navigation-item--profile-pic': activeChannelUrl, 'header__navigation-item--profile-pic': activeChannelUrl,
})} })}
// @if TARGET='app' // @if TARGET='app'
onDoubleClick={e => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
// @endif // @endif

View file

@ -22,7 +22,7 @@ type Props = {
menuButton: boolean, menuButton: boolean,
children: any, children: any,
doReadNotifications: ([number]) => void, doReadNotifications: ([number]) => void,
doDeleteNotification: number => void, doDeleteNotification: (number) => void,
}; };
export default function Notification(props: Props) { export default function Notification(props: Props) {
@ -166,10 +166,10 @@ export default function Notification(props: Props) {
<div className="notification__menu"> <div className="notification__menu">
<Menu> <Menu>
<MenuButton onClick={e => e.stopPropagation()}> <MenuButton onClick={(e) => e.stopPropagation()}>
<Icon size={18} icon={ICONS.MORE_VERTICAL} /> <Icon size={18} icon={ICONS.MORE_VERTICAL} />
</MenuButton> </MenuButton>
<MenuList className="menu__list--comments"> <MenuList className="menu__list">
<MenuItem className="menu__link" onSelect={() => doDeleteNotification(id)}> <MenuItem className="menu__link" onSelect={() => doDeleteNotification(id)}>
<Icon aria-hidden icon={ICONS.DELETE} /> <Icon aria-hidden icon={ICONS.DELETE} />
{__('Delete')} {__('Delete')}

View file

@ -272,7 +272,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} /> <PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} /> <PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} /> <PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} />
<PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} /> <PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />

View file

@ -268,6 +268,15 @@ export const COMMENT_REACT_FAILED = 'COMMENT_REACT_FAILED';
export const COMMENT_PIN_STARTED = 'COMMENT_PIN_STARTED'; export const COMMENT_PIN_STARTED = 'COMMENT_PIN_STARTED';
export const COMMENT_PIN_COMPLETED = 'COMMENT_PIN_COMPLETED'; export const COMMENT_PIN_COMPLETED = 'COMMENT_PIN_COMPLETED';
export const COMMENT_PIN_FAILED = 'COMMENT_PIN_FAILED'; export const COMMENT_PIN_FAILED = 'COMMENT_PIN_FAILED';
export const COMMENT_MODERATION_BLOCK_LIST_STARTED = 'COMMENT_MODERATION_BLOCK_LIST_STARTED';
export const COMMENT_MODERATION_BLOCK_LIST_COMPLETED = 'COMMENT_MODERATION_BLOCK_LIST_COMPLETED';
export const COMMENT_MODERATION_BLOCK_LIST_FAILED = 'COMMENT_MODERATION_BLOCK_LIST_FAILED';
export const COMMENT_MODERATION_BLOCK_STARTED = 'COMMENT_MODERATION_BLOCK_STARTED';
export const COMMENT_MODERATION_BLOCK_COMPLETE = 'COMMENT_MODERATION_BLOCK_COMPLETE';
export const COMMENT_MODERATION_BLOCK_FAILED = 'COMMENT_MODERATION_BLOCK_FAILED';
export const COMMENT_MODERATION_UN_BLOCK_STARTED = 'COMMENT_MODERATION_UN_BLOCK_STARTED';
export const COMMENT_MODERATION_UN_BLOCK_COMPLETE = 'COMMENT_MODERATION_UN_BLOCK_COMPLETE';
export const COMMENT_MODERATION_UN_BLOCK_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED';
// Blocked channels // Blocked channels
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL'; export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';

View file

@ -82,6 +82,7 @@ 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 BLOCK = 'Slash';
export const MUTE = 'VolumeX';
export const UNBLOCK = 'Circle'; export const UNBLOCK = 'Circle';
export const VIEW = 'View'; export const VIEW = 'View';
export const EYE = 'Eye'; export const EYE = 'Eye';

View file

@ -41,7 +41,6 @@ export const LIQUIDATE_SUPPORTS = 'liquidate_supports';
export const MASS_TIP_UNLOCK = 'mass_tip_unlock'; export const MASS_TIP_UNLOCK = 'mass_tip_unlock';
export const CONFIRM_AGE = 'confirm_age'; export const CONFIRM_AGE = 'confirm_age';
export const SYNC_ENABLE = 'SYNC_ENABLE'; export const SYNC_ENABLE = 'SYNC_ENABLE';
export const REMOVE_BLOCKED = 'remove_blocked';
export const IMAGE_UPLOAD = 'image_upload'; export const IMAGE_UPLOAD = 'image_upload';
export const MOBILE_SEARCH = 'mobile_search'; export const MOBILE_SEARCH = 'mobile_search';
export const VIEW_IMAGE = 'view_image'; export const VIEW_IMAGE = 'view_image';

View file

@ -24,6 +24,7 @@ exports.SEND = 'send';
exports.SETTINGS = 'settings'; exports.SETTINGS = 'settings';
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications'; exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
exports.SETTINGS_ADVANCED = 'settings/advanced'; exports.SETTINGS_ADVANCED = 'settings/advanced';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
exports.SHOW = 'show'; exports.SHOW = 'show';
exports.ACCOUNT = 'account'; exports.ACCOUNT = 'account';
exports.SEARCH = 'search'; exports.SEARCH = 'search';
@ -36,7 +37,6 @@ exports.CHANNELS_FOLLOWING = 'following';
exports.DEPRECATED__CHANNELS_FOLLOWING_MANAGE = 'following/channels/manage'; exports.DEPRECATED__CHANNELS_FOLLOWING_MANAGE = 'following/channels/manage';
exports.CHANNELS_FOLLOWING_DISCOVER = 'following/discover'; exports.CHANNELS_FOLLOWING_DISCOVER = 'following/discover';
exports.WALLET = 'wallet'; exports.WALLET = 'wallet';
exports.BLOCKED = 'blocked';
exports.CHANNELS = 'channels'; exports.CHANNELS = 'channels';
exports.EMBED = 'embed'; exports.EMBED = 'embed';
exports.TOP = 'top'; exports.TOP = 'top';

View file

@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import ChannelCreate from './view';
const select = state => ({});
const perform = {
doHideModal,
};
export default connect(
select,
perform
)(ChannelCreate);

View file

@ -1,18 +0,0 @@
// @flow
import React from 'react';
import ChannelForm from 'component/channelForm';
import { Modal } from 'modal/modal';
type Props = { doHideModal: () => void };
const ModalChannelCreate = (props: Props) => {
const { doHideModal } = props;
return (
<Modal isOpen type="card" onAborted={doHideModal}>
<ChannelForm onSuccess={doHideModal} />
</Modal>
);
};
export default ModalChannelCreate;

View file

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import { doToggleBlockChannel } from 'redux/actions/blocked';
import { selectBlockedChannels } from 'redux/selectors/blocked';
import ModalRemoveBlocked from './view';
const select = (state, props) => ({
blockedChannels: selectBlockedChannels(state),
});
const perform = dispatch => ({
closeModal: () => dispatch(doHideModal()),
toggleBlockChannel: uri => dispatch(doToggleBlockChannel(uri)),
});
export default connect(select, perform)(ModalRemoveBlocked);

View file

@ -1,46 +0,0 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import I18nMessage from 'component/i18nMessage';
type Props = {
blockedUri: string,
closeModal: () => void,
blockedChannels: Array<string>,
toggleBlockChannel: (uri: string) => void,
};
function ModalRemoveBlocked(props: Props) {
const { blockedUri, closeModal, blockedChannels, toggleBlockChannel } = props;
function handleConfirm() {
if (blockedUri && blockedChannels.includes(blockedUri)) {
// DANGER: Always ensure the uri is actually in the list since we are using a
// toggle function. If 'null' is accidentally toggled INTO the list, the app
// won't start. Ideally, we should add a "removeBlockedChannel()", but with
// the gating above, it should be safe/good enough.
toggleBlockChannel(blockedUri);
}
closeModal();
}
return (
<Modal
isOpen
type="confirm"
title={__('Remove from blocked list')}
confirmButtonLabel={__('Remove')}
onConfirmed={handleConfirm}
onAborted={() => closeModal()}
>
<em>{blockedUri}</em>
<p />
<p>
<I18nMessage>Are you sure you want to remove this from the list?</I18nMessage>
</p>
</Modal>
);
}
export default ModalRemoveBlocked;

View file

@ -16,7 +16,6 @@ import ModalRevokeClaim from 'modal/modalRevokeClaim';
import ModalPhoneCollection from 'modal/modalPhoneCollection'; import ModalPhoneCollection from 'modal/modalPhoneCollection';
import ModalFirstSubscription from 'modal/modalFirstSubscription'; import ModalFirstSubscription from 'modal/modalFirstSubscription';
import ModalConfirmTransaction from 'modal/modalConfirmTransaction'; import ModalConfirmTransaction from 'modal/modalConfirmTransaction';
import ModalRemoveBlocked from 'modal/modalRemoveBlocked';
import ModalSocialShare from 'modal/modalSocialShare'; import ModalSocialShare from 'modal/modalSocialShare';
import ModalSendTip from 'modal/modalSendTip'; import ModalSendTip from 'modal/modalSendTip';
import ModalPublish from 'modal/modalPublish'; import ModalPublish from 'modal/modalPublish';
@ -32,7 +31,6 @@ import ModalCommentAcknowledgement from 'modal/modalCommentAcknowledgement';
import ModalWalletSend from 'modal/modalWalletSend'; import ModalWalletSend from 'modal/modalWalletSend';
import ModalWalletReceive from 'modal/modalWalletReceive'; import ModalWalletReceive from 'modal/modalWalletReceive';
import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome'; import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome';
import ModalCreateChannel from 'modal/modalChannelCreate';
import ModalSetReferrer from 'modal/modalSetReferrer'; import ModalSetReferrer from 'modal/modalSetReferrer';
import ModalSignOut from 'modal/modalSignOut'; import ModalSignOut from 'modal/modalSignOut';
import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate'; import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate';
@ -130,8 +128,6 @@ function ModalRouter(props: Props) {
return <ModalWalletReceive {...modalProps} />; return <ModalWalletReceive {...modalProps} />;
case MODALS.YOUTUBE_WELCOME: case MODALS.YOUTUBE_WELCOME:
return <ModalYoutubeWelcome />; return <ModalYoutubeWelcome />;
case MODALS.CREATE_CHANNEL:
return <ModalCreateChannel {...modalProps} />;
case MODALS.SET_REFERRER: case MODALS.SET_REFERRER:
return <ModalSetReferrer {...modalProps} />; return <ModalSetReferrer {...modalProps} />;
case MODALS.SIGN_OUT: case MODALS.SIGN_OUT:
@ -142,8 +138,6 @@ function ModalRouter(props: Props) {
return <ModalFileSelection {...modalProps} />; return <ModalFileSelection {...modalProps} />;
case MODALS.LIQUIDATE_SUPPORTS: case MODALS.LIQUIDATE_SUPPORTS:
return <ModalSupportsLiquidate {...modalProps} />; return <ModalSupportsLiquidate {...modalProps} />;
case MODALS.REMOVE_BLOCKED:
return <ModalRemoveBlocked {...modalProps} />;
case MODALS.IMAGE_UPLOAD: case MODALS.IMAGE_UPLOAD:
return <ModalImageUpload {...modalProps} />; return <ModalImageUpload {...modalProps} />;
case MODALS.SYNC_ENABLE: case MODALS.SYNC_ENABLE:

View file

@ -8,10 +8,11 @@ import {
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectClaimIsPending, makeSelectClaimIsPending,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectChannelIsBlocked } from 'redux/selectors/blocked'; import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { selectBlackListedOutpoints, doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc'; import { selectBlackListedOutpoints, doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc';
import { selectYoutubeChannels } from 'redux/selectors/user'; import { selectYoutubeChannels } from 'redux/selectors/user';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { selectModerationBlockList } from 'redux/selectors/comments';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import ChannelPage from './view'; import ChannelPage from './view';
@ -23,16 +24,17 @@ const select = (state, props) => ({
page: selectCurrentChannelPage(state), page: selectCurrentChannelPage(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state), isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
channelIsBlocked: selectChannelIsBlocked(props.uri)(state), channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state), blackListedOutpoints: selectBlackListedOutpoints(state),
subCount: makeSelectSubCountForUri(props.uri)(state), subCount: makeSelectSubCountForUri(props.uri)(state),
pending: makeSelectClaimIsPending(props.uri)(state), pending: makeSelectClaimIsPending(props.uri)(state),
youtubeChannels: selectYoutubeChannels(state), youtubeChannels: selectYoutubeChannels(state),
blockedChannels: selectModerationBlockList(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
fetchSubCount: claimId => dispatch(doFetchSubCount(claimId)), fetchSubCount: (claimId) => dispatch(doFetchSubCount(claimId)),
}); });
export default connect(select, perform)(ChannelPage); export default connect(select, perform)(ChannelPage);

View file

@ -6,7 +6,6 @@ import { parseURI } from 'lbry-redux';
import { YOUTUBE_STATUSES } from 'lbryinc'; import { YOUTUBE_STATUSES } from 'lbryinc';
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 { useHistory } from 'react-router'; import { useHistory } from 'react-router';
@ -21,6 +20,8 @@ import classnames from 'classnames';
import HelpLink from 'component/common/help-link'; import HelpLink from 'component/common/help-link';
import ClaimSupportButton from 'component/claimSupportButton'; import ClaimSupportButton from 'component/claimSupportButton';
import ChannelStakedIndicator from 'component/channelStakedIndicator'; import ChannelStakedIndicator from 'component/channelStakedIndicator';
import ClaimMenuList from 'component/claimMenuList';
import Yrbl from 'component/yrbl';
export const PAGE_VIEW_QUERY = `view`; export const PAGE_VIEW_QUERY = `view`;
const ABOUT_PAGE = `about`; const ABOUT_PAGE = `about`;
@ -46,6 +47,7 @@ type Props = {
subCount: number, subCount: number,
pending: boolean, pending: boolean,
youtubeChannels: ?Array<{ channel_claim_id: string, sync_status: string, transfer_state: string }>, youtubeChannels: ?Array<{ channel_claim_id: string, sync_status: string, transfer_state: string }>,
blockedChannels: Array<string>,
}; };
function ChannelPage(props: Props) { function ChannelPage(props: Props) {
@ -63,12 +65,14 @@ function ChannelPage(props: Props) {
subCount, subCount,
pending, pending,
youtubeChannels, youtubeChannels,
blockedChannels,
} = props; } = props;
const { const {
push, push,
goBack, goBack,
location: { search }, location: { search },
} = useHistory(); } = useHistory();
const [viewBlockedChannel, setViewBlockedChannel] = React.useState(false);
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined; const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
const [discussionWasMounted, setDiscussionWasMounted] = React.useState(false); const [discussionWasMounted, setDiscussionWasMounted] = React.useState(false);
@ -77,6 +81,7 @@ function ChannelPage(props: Props) {
const { permanent_url: permanentUrl } = claim; const { permanent_url: permanentUrl } = claim;
const claimId = claim.claim_id; const claimId = claim.claim_id;
const formattedSubCount = Number(subCount).toLocaleString(); const formattedSubCount = Number(subCount).toLocaleString();
const isBlocked = claim && blockedChannels.includes(claim.permanent_url);
const isMyYouTubeChannel = const isMyYouTubeChannel =
claim && claim &&
youtubeChannels && youtubeChannels &&
@ -157,7 +162,7 @@ function ChannelPage(props: Props) {
{!channelIsBlocked && !channelIsBlackListed && <ShareButton uri={uri} />} {!channelIsBlocked && !channelIsBlackListed && <ShareButton uri={uri} />}
{!channelIsBlocked && <ClaimSupportButton uri={uri} />} {!channelIsBlocked && <ClaimSupportButton uri={uri} />}
{!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />} {!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}
{!isSubscribed && <BlockButton uri={permanentUrl} />} <ClaimMenuList uri={claim.permanent_url} inline />
</div> </div>
{cover && ( {cover && (
<img <img
@ -203,24 +208,44 @@ function ChannelPage(props: Props) {
<div className="channel-cover__gradient" /> <div className="channel-cover__gradient" />
</header> </header>
<Tabs onChange={onTabChange} index={tabIndex}> {isBlocked && !viewBlockedChannel ? (
<TabList className="tabs__list--channel-page"> <div className="main--empty">
<Tab disabled={editing}>{__('Content')}</Tab> <Yrbl
<Tab>{editing ? __('Editing Your Channel') : __('About --[tab title in Channel Page]--')}</Tab> title={__('This channel is blocked')}
<Tab disabled={editing}>{__('Community')}</Tab> subtitle={__('Are you sure you want to view this content? Viewing will not unblock @%channel%', {
</TabList> channel: channelName,
<TabPanels> })}
<TabPanel> actions={
<ChannelContent uri={uri} channelIsBlackListed={channelIsBlackListed} /> <div className="section__actions">
</TabPanel> <Button button="primary" label={__('View Content')} onClick={() => setViewBlockedChannel(true)} />
<TabPanel> </div>
<ChannelAbout uri={uri} /> }
</TabPanel> />
<TabPanel> </div>
{(discussionWasMounted || currentView === DISCUSSION_PAGE) && <ChannelDiscussion uri={uri} />} ) : (
</TabPanel> <Tabs onChange={onTabChange} index={tabIndex}>
</TabPanels> <TabList className="tabs__list--channel-page">
</Tabs> <Tab disabled={editing}>{__('Content')}</Tab>
<Tab>{editing ? __('Editing Your Channel') : __('About --[tab title in Channel Page]--')}</Tab>
<Tab disabled={editing}>{__('Community')}</Tab>
</TabList>
<TabPanels>
<TabPanel>
<ChannelContent
uri={uri}
channelIsBlackListed={channelIsBlackListed}
viewBlockedChannel={viewBlockedChannel}
/>
</TabPanel>
<TabPanel>
<ChannelAbout uri={uri} />
</TabPanel>
<TabPanel>
{(discussionWasMounted || currentView === DISCUSSION_PAGE) && <ChannelDiscussion uri={uri} />}
</TabPanel>
</TabPanels>
</Tabs>
)}
</Page> </Page>
); );
} }

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux'; import { selectBalance } from 'lbry-redux';
import ChannelNew from './view'; import ChannelNew from './view';
const select = state => ({ const select = (state) => ({
balance: selectBalance(state), balance: selectBalance(state),
}); });

View file

@ -21,7 +21,12 @@ function ChannelNew(props: Props) {
<Page noSideNavigation noFooter backout={{ title: __('Create a channel'), backLabel: __('Cancel') }}> <Page noSideNavigation noFooter backout={{ title: __('Create a channel'), backLabel: __('Cancel') }}>
{emptyBalance && <YrblWalletEmpty />} {emptyBalance && <YrblWalletEmpty />}
<ChannelEdit disabled={emptyBalance} onDone={() => push(redirectUrl || `/$/${PAGES.CHANNELS}`)} /> <ChannelEdit
disabled={emptyBalance}
onDone={() => {
push(redirectUrl || `/$/${PAGES.CHANNELS}`);
}}
/>
</Page> </Page>
); );
} }

View file

@ -24,14 +24,14 @@ export default function ChannelsPage(props: Props) {
const { channels, channelUrls, fetchChannelListMine, fetchingChannels, youtubeChannels } = props; const { channels, channelUrls, fetchChannelListMine, fetchingChannels, youtubeChannels } = props;
const [rewardData, setRewardData] = React.useState(); const [rewardData, setRewardData] = React.useState();
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length); const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
const hasPendingChannels = channels && channels.some(channel => channel.confirmations < 0); const hasPendingChannels = channels && channels.some((channel) => channel.confirmations < 0);
useEffect(() => { useEffect(() => {
fetchChannelListMine(); fetchChannelListMine();
}, [fetchChannelListMine, hasPendingChannels]); }, [fetchChannelListMine, hasPendingChannels]);
useEffect(() => { useEffect(() => {
Lbryio.call('user_rewards', 'view_rate').then(data => setRewardData(data)); Lbryio.call('user_rewards', 'view_rate').then((data) => setRewardData(data));
}, [setRewardData]); }, [setRewardData]);
return ( return (
@ -52,7 +52,7 @@ export default function ChannelsPage(props: Props) {
} }
loading={fetchingChannels} loading={fetchingChannels}
uris={channelUrls} uris={channelUrls}
renderActions={claim => { renderActions={(claim) => {
const claimsInChannel = claim.meta.claims_in_channel; const claimsInChannel = claim.meta.claims_in_channel;
return claimsInChannel === 0 ? ( return claimsInChannel === 0 ? (
<span /> <span />
@ -67,7 +67,7 @@ export default function ChannelsPage(props: Props) {
</div> </div>
); );
}} }}
renderProperties={claim => { renderProperties={(claim) => {
const claimsInChannel = claim.meta.claims_in_channel; const claimsInChannel = claim.meta.claims_in_channel;
if (!claim || claimsInChannel === 0) { if (!claim || claimsInChannel === 0) {
return null; return null;
@ -76,7 +76,7 @@ export default function ChannelsPage(props: Props) {
const channelRewardData = const channelRewardData =
rewardData && rewardData &&
rewardData.rates && rewardData.rates &&
rewardData.rates.find(data => { rewardData.rates.find((data) => {
return data.channel_claim_id === claim.claim_id; return data.channel_claim_id === claim.claim_id;
}); });

View file

@ -1,14 +1,14 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectFollowedTags } from 'redux/selectors/tags'; import { selectFollowedTags } from 'redux/selectors/tags';
import { selectBlockedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectHomepageData } from 'redux/selectors/settings'; import { selectHomepageData } from 'redux/selectors/settings';
import ChannelsFollowingManagePage from './view'; import ChannelsFollowingManagePage from './view';
const select = state => ({ const select = (state) => ({
followedTags: selectFollowedTags(state), followedTags: selectFollowedTags(state),
subscribedChannels: selectSubscriptions(state), subscribedChannels: selectSubscriptions(state),
blockedChannels: selectBlockedChannels(state), blockedChannels: selectMutedChannels(state),
homepageData: selectHomepageData(state), homepageData: selectHomepageData(state),
}); });

View file

@ -1,9 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectBlockedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectModerationBlockList, selectFetchingModerationBlockList } from 'redux/selectors/comments';
import ListBlocked from './view'; import ListBlocked from './view';
const select = state => ({ const select = (state) => ({
uris: selectBlockedChannels(state), mutedUris: selectMutedChannels(state),
blockedUris: selectModerationBlockList(state),
fetchingModerationBlockList: selectFetchingModerationBlockList(state),
}); });
export default connect(select, null)(ListBlocked); export default connect(select)(ListBlocked);

View file

@ -1,34 +1,141 @@
// @flow // @flow
import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import classnames from 'classnames';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import Page from 'component/page'; import Page from 'component/page';
import Card from 'component/common/card'; import Spinner from 'component/spinner';
import Button from 'component/button';
import usePrevious from 'effects/use-previous';
import usePersistedState from 'effects/use-persisted-state';
import ChannelBlockButton from 'component/channelBlockButton';
import ChannelMuteButton from 'component/channelMuteButton';
type Props = { type Props = {
uris: Array<string>, mutedUris: ?Array<string>,
blockedUris: ?Array<string>,
fetchingModerationBlockList: boolean,
}; };
const VIEW_BLOCKED = 'blocked';
const VIEW_MUTED = 'muted';
function ListBlocked(props: Props) { function ListBlocked(props: Props) {
const { uris } = props; const { mutedUris, blockedUris, fetchingModerationBlockList } = props;
const [viewMode, setViewMode] = usePersistedState('blocked-muted:display', VIEW_BLOCKED);
const [loading, setLoading] = React.useState(!blockedUris || !blockedUris.length);
// Keep a local list to allow for undoing actions in this component
const [localBlockedList, setLocalBlockedList] = React.useState(undefined);
const [localMutedList, setLocalMutedList] = React.useState(undefined);
const previousFetchingModBlockList = usePrevious(fetchingModerationBlockList);
const hasLocalMuteList = localMutedList && localMutedList.length > 0;
const hasLocalBlockList = localBlockedList && localBlockedList.length > 0;
const stringifiedMutedChannels = JSON.stringify(mutedUris);
const justMuted = localMutedList && mutedUris && localMutedList.length < mutedUris.length;
const justBlocked = localBlockedList && blockedUris && localBlockedList.length < blockedUris.length;
const stringifiedBlockedChannels = JSON.stringify(blockedUris);
React.useEffect(() => {
if (previousFetchingModBlockList && !fetchingModerationBlockList) {
setLoading(false);
}
}, [setLoading, previousFetchingModBlockList, fetchingModerationBlockList]);
React.useEffect(() => {
const jsonMutedChannels = stringifiedMutedChannels && JSON.parse(stringifiedMutedChannels);
if (!hasLocalMuteList && jsonMutedChannels && jsonMutedChannels.length > 0) {
setLocalMutedList(jsonMutedChannels);
}
}, [stringifiedMutedChannels, hasLocalMuteList]);
React.useEffect(() => {
const jsonBlockedChannels = stringifiedBlockedChannels && JSON.parse(stringifiedBlockedChannels);
if (!hasLocalBlockList && jsonBlockedChannels && jsonBlockedChannels.length > 0) {
setLocalBlockedList(jsonBlockedChannels);
}
}, [stringifiedBlockedChannels, hasLocalBlockList]);
React.useEffect(() => {
if (justMuted && stringifiedMutedChannels) {
setLocalMutedList(JSON.parse(stringifiedMutedChannels));
}
}, [stringifiedMutedChannels, justMuted, setLocalMutedList]);
React.useEffect(() => {
if (justBlocked && stringifiedBlockedChannels) {
setLocalBlockedList(JSON.parse(stringifiedBlockedChannels));
}
}, [stringifiedBlockedChannels, justBlocked, setLocalBlockedList]);
return ( return (
<Page> <Page>
{uris && uris.length ? ( {loading && (
<Card
isBodyList
title={__('Your blocked channels')}
body={<ClaimList uris={uris} showUnresolvedClaims showHiddenByUser />}
/>
) : (
<div className="main--empty"> <div className="main--empty">
<section className="card card--section"> <Spinner />
<h2 className="card__title card__title--deprecated">{__('You arent blocking any channels')}</h2>
<p className="section__subtitle">
{__('When you block a channel, all content from that channel will be hidden.')}
</p>
</section>
</div> </div>
)} )}
{!loading && (
<>
<div className="section__header--actions">
<div className="section__actions--inline">
<Button
icon={ICONS.BLOCK}
button="alt"
label={__('Blocked')}
className={classnames(`button-toggle`, {
'button-toggle--active': viewMode === VIEW_BLOCKED,
})}
onClick={() => setViewMode(VIEW_BLOCKED)}
/>
<Button
icon={ICONS.MUTE}
button="alt"
label={__('Muted')}
className={classnames(`button-toggle`, {
'button-toggle--active': viewMode === VIEW_MUTED,
})}
onClick={() => setViewMode(VIEW_MUTED)}
/>
</div>
</div>
<div className="help--notice">
{viewMode === VIEW_MUTED
? __(
'Muted channels will be invisible to you in the app. They will not know they are muted and can still interact with you and your content.'
)
: __(
"Blocked channels will be invisible to you in the app. They will not be able to comment on your content, or reply to you comments left on other channels' content."
)}
</div>
<ClaimList
uris={viewMode === VIEW_MUTED ? localMutedList : localBlockedList}
showUnresolvedClaims
showHiddenByUser
hideMenu
renderActions={(claim) => {
return (
<div className="section__actions">
{viewMode === VIEW_MUTED ? (
<>
<ChannelMuteButton uri={claim.permanent_url} />
<ChannelBlockButton uri={claim.permanent_url} />
</>
) : (
<>
<ChannelBlockButton uri={claim.permanent_url} />
<ChannelMuteButton uri={claim.permanent_url} />
</>
)}
</div>
);
}}
/>
</>
)}
</Page> </Page>
); );
} }

View file

@ -12,11 +12,10 @@ import {
import { doSetPlayingUri } from 'redux/actions/content'; import { doSetPlayingUri } from 'redux/actions/content';
import { makeSelectClientSetting, selectDaemonSettings, selectLanguage } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectDaemonSettings, selectLanguage } from 'redux/selectors/settings';
import { doWalletStatus, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux'; import { doWalletStatus, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
import { selectBlockedChannelsCount } from 'redux/selectors/blocked';
import SettingsPage from './view'; import SettingsPage from './view';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
const select = state => ({ const select = (state) => ({
daemonSettings: selectDaemonSettings(state), daemonSettings: selectDaemonSettings(state),
allowAnalytics: selectAllowAnalytics(state), allowAnalytics: selectAllowAnalytics(state),
isAuthenticated: selectUserVerifiedEmail(state), isAuthenticated: selectUserVerifiedEmail(state),
@ -27,7 +26,6 @@ const select = state => ({
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state), autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
walletEncrypted: selectWalletIsEncrypted(state), walletEncrypted: selectWalletIsEncrypted(state),
autoDownload: makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state), autoDownload: makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state),
userBlockedChannelsCount: selectBlockedChannelsCount(state),
hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state), hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state),
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state), floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
@ -35,14 +33,14 @@ const select = state => ({
language: selectLanguage(state), language: selectLanguage(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)), setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
clearDaemonSetting: key => dispatch(doClearDaemonSetting(key)), clearDaemonSetting: (key) => dispatch(doClearDaemonSetting(key)),
toggle3PAnalytics: allow => dispatch(doToggle3PAnalytics(allow)), toggle3PAnalytics: (allow) => dispatch(doToggle3PAnalytics(allow)),
clearCache: () => dispatch(doClearCache()), clearCache: () => dispatch(doClearCache()),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
updateWalletStatus: () => dispatch(doWalletStatus()), updateWalletStatus: () => dispatch(doWalletStatus()),
confirmForgetPassword: modalProps => dispatch(doNotifyForgetPassword(modalProps)), confirmForgetPassword: (modalProps) => dispatch(doNotifyForgetPassword(modalProps)),
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })), clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
setDarkTime: (time, options) => dispatch(doSetDarkTime(time, options)), setDarkTime: (time, options) => dispatch(doSetDarkTime(time, options)),
openModal: (id, params) => dispatch(doOpenModal(id, params)), openModal: (id, params) => dispatch(doOpenModal(id, params)),

View file

@ -44,9 +44,9 @@ type DaemonSettings = {
type Props = { type Props = {
setDaemonSetting: (string, ?SetDaemonSettingArg) => void, setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
clearDaemonSetting: string => void, clearDaemonSetting: (string) => void,
setClientSetting: (string, SetDaemonSettingArg) => void, setClientSetting: (string, SetDaemonSettingArg) => void,
toggle3PAnalytics: boolean => void, toggle3PAnalytics: (boolean) => void,
clearCache: () => Promise<any>, clearCache: () => Promise<any>,
daemonSettings: DaemonSettings, daemonSettings: DaemonSettings,
allowAnalytics: boolean, allowAnalytics: boolean,
@ -60,14 +60,13 @@ type Props = {
autoplay: boolean, autoplay: boolean,
updateWalletStatus: () => void, updateWalletStatus: () => void,
walletEncrypted: boolean, walletEncrypted: boolean,
userBlockedChannelsCount?: number,
confirmForgetPassword: ({}) => void, confirmForgetPassword: ({}) => void,
floatingPlayer: boolean, floatingPlayer: boolean,
hideReposts: ?boolean, hideReposts: ?boolean,
clearPlayingUri: () => void, clearPlayingUri: () => void,
darkModeTimes: DarkModeTimes, darkModeTimes: DarkModeTimes,
setDarkTime: (string, {}) => void, setDarkTime: (string, {}) => void,
openModal: string => void, openModal: (string) => void,
language?: string, language?: string,
enterSettings: () => void, enterSettings: () => void,
exitSettings: () => void, exitSettings: () => void,
@ -98,7 +97,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
if (isAuthenticated || !IS_WEB) { if (isAuthenticated || !IS_WEB) {
this.props.updateWalletStatus(); this.props.updateWalletStatus();
getPasswordFromCookie().then(p => { getPasswordFromCookie().then((p) => {
if (typeof p === 'string') { if (typeof p === 'string') {
this.setState({ storedPassword: true }); this.setState({ storedPassword: true });
} }
@ -172,7 +171,6 @@ class SettingsPage extends React.PureComponent<Props, State> {
setDaemonSetting, setDaemonSetting,
setClientSetting, setClientSetting,
toggle3PAnalytics, toggle3PAnalytics,
userBlockedChannelsCount,
floatingPlayer, floatingPlayer,
hideReposts, hideReposts,
clearPlayingUri, clearPlayingUri,
@ -262,7 +260,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
value={currentTheme} value={currentTheme}
disabled={automaticDarkModeEnabled} disabled={automaticDarkModeEnabled}
> >
{themes.map(theme => ( {themes.map((theme) => (
<option key={theme} value={theme}> <option key={theme} value={theme}>
{theme === 'light' ? __('Light') : __('Dark')} {theme === 'light' ? __('Light') : __('Dark')}
</option> </option>
@ -282,11 +280,11 @@ class SettingsPage extends React.PureComponent<Props, State> {
<FormField <FormField
type="select" type="select"
name="automatic_dark_mode_range" name="automatic_dark_mode_range"
onChange={value => this.onChangeTime(value, { fromTo: 'from', time: 'hour' })} onChange={(value) => this.onChangeTime(value, { fromTo: 'from', time: 'hour' })}
value={darkModeTimes.from.hour} value={darkModeTimes.from.hour}
label={__('From --[initial time]--')} label={__('From --[initial time]--')}
> >
{startHours.map(time => ( {startHours.map((time) => (
<option key={time} value={time}> <option key={time} value={time}>
{this.to12Hour(time)} {this.to12Hour(time)}
</option> </option>
@ -296,10 +294,10 @@ class SettingsPage extends React.PureComponent<Props, State> {
type="select" type="select"
name="automatic_dark_mode_range" name="automatic_dark_mode_range"
label={__('To --[final time]--')} label={__('To --[final time]--')}
onChange={value => this.onChangeTime(value, { fromTo: 'to', time: 'hour' })} onChange={(value) => this.onChangeTime(value, { fromTo: 'to', time: 'hour' })}
value={darkModeTimes.to.hour} value={darkModeTimes.to.hour}
> >
{endHours.map(time => ( {endHours.map((time) => (
<option key={time} value={time}> <option key={time} value={time}>
{this.to12Hour(time)} {this.to12Hour(time)}
</option> </option>
@ -342,7 +340,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
<FormField <FormField
type="checkbox" type="checkbox"
name="hide_reposts" name="hide_reposts"
onChange={e => { onChange={(e) => {
if (isAuthenticated) { if (isAuthenticated) {
let param = e.target.checked ? { add: 'noreposts' } : { remove: 'noreposts' }; let param = e.target.checked ? { add: 'noreposts' } : { remove: 'noreposts' };
Lbryio.call('user_tag', 'edit', param); Lbryio.call('user_tag', 'edit', param);
@ -411,7 +409,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
<FormField <FormField
type="checkbox" type="checkbox"
name="share_third_party" name="share_third_party"
onChange={e => toggle3PAnalytics(e.target.checked)} onChange={(e) => toggle3PAnalytics(e.target.checked)}
checked={allowAnalytics} checked={allowAnalytics}
label={__('Allow the app to access third party analytics platforms')} label={__('Allow the app to access third party analytics platforms')}
helper={__('We use detailed analytics to improve all aspects of the LBRY experience.')} helper={__('We use detailed analytics to improve all aspects of the LBRY experience.')}
@ -438,21 +436,14 @@ class SettingsPage extends React.PureComponent<Props, State> {
/> />
<Card <Card
title={__('Blocked channels')} title={__('Blocked and muted channels')}
subtitle={
userBlockedChannelsCount === 0
? __("You don't have blocked channels.")
: userBlockedChannelsCount === 1
? __('You have one blocked channel.')
: __('You have %channels% blocked channels.', { channels: userBlockedChannelsCount })
}
actions={ actions={
<div className="section__actions"> <div className="section__actions">
<Button <Button
button="secondary" button="secondary"
label={__('Manage')} label={__('Manage')}
icon={ICONS.SETTINGS} icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.BLOCKED}`} navigate={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`}
/> />
</div> </div>
} }

View file

@ -3,7 +3,7 @@ import * as ACTIONS from 'constants/action_types';
import { selectPrefsReady } from 'redux/selectors/sync'; import { selectPrefsReady } from 'redux/selectors/sync';
import { doAlertWaitingForSync } from 'redux/actions/app'; import { doAlertWaitingForSync } from 'redux/actions/app';
export const doToggleBlockChannel = (uri: string) => (dispatch: Dispatch, getState: GetState) => { export const doToggleMuteChannel = (uri: string) => (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const ready = selectPrefsReady(state); const ready = selectPrefsReady(state);

View file

@ -1,13 +1,14 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as REACTION_TYPES from 'constants/reactions'; import * as REACTION_TYPES from 'constants/reactions';
import { Lbry, selectClaimsByUri } from 'lbry-redux'; import { Lbry, parseURI, buildURI, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux';
import { doToast, doSeeNotifications } from 'redux/actions/notifications'; import { doToast, doSeeNotifications } from 'redux/actions/notifications';
import { import {
makeSelectCommentIdsForUri, makeSelectCommentIdsForUri,
makeSelectMyReactionsForComment, makeSelectMyReactionsForComment,
makeSelectOthersReactionsForComment, makeSelectOthersReactionsForComment,
selectPendingCommentReacts, selectPendingCommentReacts,
selectModerationBlockList,
} from 'redux/selectors/comments'; } from 'redux/selectors/comments';
import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications'; import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
@ -50,7 +51,7 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
}); });
return result; return result;
}) })
.catch(error => { .catch((error) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_LIST_FAILED, type: ACTIONS.COMMENT_LIST_FAILED,
data: error, data: error,
@ -89,7 +90,7 @@ export function doCommentReactList(uri: string | null, commentId?: string) {
}, },
}); });
}) })
.catch(error => { .catch((error) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_FAILED, type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
data: error, data: error,
@ -171,14 +172,14 @@ export function doCommentReact(commentId: string, type: string) {
data: commentId + type, data: commentId + type,
}); });
}) })
.catch(error => { .catch((error) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_REACT_FAILED, type: ACTIONS.COMMENT_REACT_FAILED,
data: commentId + type, data: commentId + type,
}); });
const myRevertedReactsObj = myReacts const myRevertedReactsObj = myReacts
.filter(el => el !== type) .filter((el) => el !== type)
.reduce((acc, el) => { .reduce((acc, el) => {
acc[el] = 1; acc[el] = 1;
return acc; return acc;
@ -233,14 +234,20 @@ export function doCommentCreate(comment: string = '', claim_id: string = '', par
}); });
return result; return result;
}) })
.catch(error => { .catch((error) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_CREATE_FAILED, type: ACTIONS.COMMENT_CREATE_FAILED,
data: error, data: error,
}); });
let toastMessage = __('Unable to create comment, please try again later.');
if (error && error.message === 'channel is blocked by publisher') {
toastMessage = __('Unable to comment. This channel has blocked you.');
}
dispatch( dispatch(
doToast({ doToast({
message: 'Unable to create comment, please try again later.', message: toastMessage,
isError: true, isError: true,
}) })
); );
@ -274,7 +281,7 @@ export function doCommentPin(commentId: string, remove: boolean) {
data: result, data: result,
}); });
}) })
.catch(error => { .catch((error) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_PIN_FAILED, type: ACTIONS.COMMENT_PIN_FAILED,
data: error, data: error,
@ -339,7 +346,7 @@ export function doCommentAbandon(commentId: string, creatorChannelUri?: string)
); );
} }
}) })
.catch(error => { .catch((error) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_ABANDON_FAILED, type: ACTIONS.COMMENT_ABANDON_FAILED,
data: error, data: error,
@ -389,7 +396,7 @@ export function doCommentUpdate(comment_id: string, comment: string) {
); );
} }
}) })
.catch(error => { .catch((error) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_UPDATE_FAILED, type: ACTIONS.COMMENT_UPDATE_FAILED,
data: error, data: error,
@ -406,38 +413,203 @@ export function doCommentUpdate(comment_id: string, comment: string) {
} }
// Hides a users comments from all creator's claims and prevent them from commenting in the future // Hides a users comments from all creator's claims and prevent them from commenting in the future
export function doCommentModBlock(commentAuthor: string) { export function doCommentModToggleBlock(channelUri: string, unblock: boolean = false) {
return async (dispatch: Dispatch, getState: GetState) => { return async (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const claim = selectClaimsByUri(state)[commentAuthor]; const myChannels = selectMyChannelClaims(state);
const claim = selectClaimsByUri(state)[channelUri];
if (!claim) { if (!claim) {
console.error("Can't find claim to block"); // eslint-disable-line console.error("Can't find claim to block"); // eslint-disable-line
return; return;
} }
const creatorIdToBan = claim ? claim.claim_id : null; dispatch({
const creatorNameToBan = claim ? claim.name : null; type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_STARTED : ACTIONS.COMMENT_MODERATION_BLOCK_STARTED,
const activeChannelClaim = selectActiveChannelClaim(state); data: {
uri: channelUri,
},
});
let channelSignature = {}; const creatorIdForAction = claim ? claim.claim_id : null;
if (activeChannelClaim) { const creatorNameForAction = claim ? claim.name : null;
try {
channelSignature = await Lbry.channel_sign({ let channelSignatures = [];
channel_id: activeChannelClaim.claim_id, if (myChannels) {
hexdata: toHex(activeChannelClaim.name), for (const channelClaim of myChannels) {
}); try {
} catch (e) {} const channelSignature = await Lbry.channel_sign({
channel_id: channelClaim.claim_id,
hexdata: toHex(channelClaim.name),
});
channelSignatures.push({ ...channelSignature, claim_id: channelClaim.claim_id, name: channelClaim.name });
} catch (e) {}
}
} }
return Comments.moderation_block({ const sharedModBlockParams = unblock
mod_channel_id: activeChannelClaim.claim_id, ? {
mod_channel_name: activeChannelClaim.name, un_blocked_channel_id: creatorIdForAction,
signature: channelSignature.signature, un_blocked_channel_name: creatorNameForAction,
signing_ts: channelSignature.signing_ts, }
banned_channel_id: creatorIdToBan, : {
banned_channel_name: creatorNameToBan, blocked_channel_id: creatorIdForAction,
delete_all: true, blocked_channel_name: creatorNameForAction,
}); };
const commentAction = unblock ? Comments.moderation_unblock : Comments.moderation_block;
return Promise.all(
channelSignatures.map((signatureData) =>
commentAction({
mod_channel_id: signatureData.claim_id,
mod_channel_name: signatureData.name,
signature: signatureData.signature,
signing_ts: signatureData.signing_ts,
...sharedModBlockParams,
})
)
)
.then(() => {
dispatch({
type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_COMPLETE : ACTIONS.COMMENT_MODERATION_BLOCK_COMPLETE,
data: { channelUri },
});
if (!unblock) {
dispatch(doToast({ message: __('Channel blocked. You will not see them again.') }));
}
})
.catch(() => {
dispatch({
type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_FAILED : ACTIONS.COMMENT_MODERATION_BLOCK_FAILED,
});
});
}; };
} }
export function doCommentModBlock(commentAuthor: string) {
return (dispatch: Dispatch) => {
return dispatch(doCommentModToggleBlock(commentAuthor));
};
}
export function doCommentModUnBlock(commentAuthor: string) {
return (dispatch: Dispatch) => {
return dispatch(doCommentModToggleBlock(commentAuthor, true));
};
}
export function doFetchModBlockedList() {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const myChannels = selectMyChannelClaims(state);
dispatch({
type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_STARTED,
});
let channelSignatures = [];
if (myChannels) {
for (const channelClaim of myChannels) {
try {
const channelSignature = await Lbry.channel_sign({
channel_id: channelClaim.claim_id,
hexdata: toHex(channelClaim.name),
});
channelSignatures.push({ ...channelSignature, claim_id: channelClaim.claim_id, name: channelClaim.name });
} catch (e) {}
}
}
return Promise.all(
channelSignatures.map((signatureData) =>
Comments.moderation_block_list({
mod_channel_id: signatureData.claim_id,
mod_channel_name: signatureData.name,
signature: signatureData.signature,
signing_ts: signatureData.signing_ts,
})
)
)
.then((blockLists) => {
let globalBlockList = [];
blockLists
.sort((a, b) => {
return 1;
})
.forEach((channelBlockListData) => {
const blockListForChannel = channelBlockListData && channelBlockListData.blocked_channels;
if (blockListForChannel) {
blockListForChannel.forEach((blockedChannel) => {
if (blockedChannel.blocked_channel_name) {
const channelUri = buildURI({
channelName: blockedChannel.blocked_channel_name,
claimId: blockedChannel.blocked_channel_id,
});
if (!globalBlockList.find((blockedChannel) => blockedChannel.channelUri === channelUri)) {
globalBlockList.push({ channelUri, blockedAt: blockedChannel.blocked_at });
}
}
});
}
});
dispatch({
type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED,
data: {
blockList: globalBlockList
.sort((a, b) => new Date(a.blockedAt) - new Date(b.blockedAt))
.map((blockedChannel) => blockedChannel.channelUri),
},
});
})
.catch(() => {
dispatch({
type: ACTIONS.COMMENT_MODERATION_BLOCK_LIST_FAILED,
});
});
};
}
export const doUpdateBlockListForPublishedChannel = (channelClaim: ChannelClaim) => {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const blockedUris = selectModerationBlockList(state);
let channelSignature: ?{
signature: string,
signing_ts: string,
};
try {
channelSignature = await Lbry.channel_sign({
channel_id: channelClaim.claim_id,
hexdata: toHex(channelClaim.name),
});
} catch (e) {}
if (!channelSignature) {
return;
}
return Promise.all(
blockedUris.map((uri) => {
const { channelName, channelClaimId } = parseURI(uri);
return Comments.moderation_block({
mod_channel_id: channelClaim.claim_id,
mod_channel_name: channelClaim.name,
// $FlowFixMe
signature: channelSignature.signature,
// $FlowFixMe
signing_ts: channelSignature.signing_ts,
blocked_channel_id: channelClaimId,
blocked_channel_name: channelName,
});
})
);
};
};

View file

@ -15,9 +15,9 @@ export default handleActions(
let newBlockedChannels = blockedChannels.slice(); let newBlockedChannels = blockedChannels.slice();
if (newBlockedChannels.includes(uri)) { if (newBlockedChannels.includes(uri)) {
newBlockedChannels = newBlockedChannels.filter(id => id !== uri); newBlockedChannels = newBlockedChannels.filter((id) => id !== uri);
} else { } else {
newBlockedChannels.push(uri); newBlockedChannels.unshift(uri);
} }
return { return {
@ -29,7 +29,7 @@ export default handleActions(
action: { data: { blocked: ?Array<string> } } action: { data: { blocked: ?Array<string> } }
) => { ) => {
const { blocked } = action.data; const { blocked } = action.data;
const sanitizedBlocked = blocked && blocked.filter(e => typeof e === 'string'); const sanitizedBlocked = blocked && blocked.filter((e) => typeof e === 'string');
return { return {
...state, ...state,
blockedChannels: sanitizedBlocked && sanitizedBlocked.length ? sanitizedBlocked : state.blockedChannels, blockedChannels: sanitizedBlocked && sanitizedBlocked.length ? sanitizedBlocked : state.blockedChannels,

View file

@ -16,6 +16,10 @@ const defaultState: CommentsState = {
typesReacting: [], typesReacting: [],
myReactsByCommentId: undefined, myReactsByCommentId: undefined,
othersReactsByCommentId: undefined, othersReactsByCommentId: undefined,
moderationBlockList: undefined,
fetchingModerationBlockList: false,
blockingByUri: {},
unBlockingByUri: {},
}; };
export default handleActions( export default handleActions(
@ -144,7 +148,7 @@ export default handleActions(
}; };
}, },
[ACTIONS.COMMENT_LIST_STARTED]: state => ({ ...state, isLoading: true }), [ACTIONS.COMMENT_LIST_STARTED]: (state) => ({ ...state, isLoading: true }),
[ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => { [ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => {
const { comments, claimId, uri } = action.data; const { comments, claimId, uri } = action.data;
@ -227,17 +231,15 @@ export default handleActions(
isLoading: false, isLoading: false,
}; };
}, },
// do nothing
[ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({ [ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({
...state, ...state,
isCommenting: false, isCommenting: false,
}), }),
// do nothing
[ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({ [ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({
...state, ...state,
isCommenting: true, isCommenting: true,
}), }),
// replace existing comment with comment returned here under its comment_id
[ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => { [ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => {
const { comment } = action.data; const { comment } = action.data;
const commentById = Object.assign({}, state.commentById); const commentById = Object.assign({}, state.commentById);
@ -249,25 +251,100 @@ export default handleActions(
isCommenting: false, isCommenting: false,
}; };
}, },
// nothing can be done here
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({ [ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
...state, ...state,
isCmmenting: false, isCmmenting: false,
}), }),
// nothing can really be done here [ACTIONS.COMMENT_MODERATION_BLOCK_LIST_STARTED]: (state: CommentsState, action: any) => ({
[ACTIONS.COMMENT_HIDE_STARTED]: (state: CommentsState, action: any) => ({
...state, ...state,
isLoading: true, fetchingModerationBlockList: true,
}), }),
[ACTIONS.COMMENT_HIDE_COMPLETED]: (state: CommentsState, action: any) => ({ [ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED]: (state: CommentsState, action: any) => {
...state, // todo: add HiddenComments state & create selectors const { blockList } = action.data;
isLoading: false,
}), return {
// nothing can be done here ...state,
[ACTIONS.COMMENT_HIDE_FAILED]: (state: CommentsState, action: any) => ({ moderationBlockList: blockList,
fetchingModerationBlockList: false,
};
},
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_FAILED]: (state: CommentsState, action: any) => ({
...state, ...state,
isLoading: false, fetchingModerationBlockList: false,
}), }),
[ACTIONS.COMMENT_MODERATION_BLOCK_STARTED]: (state: CommentsState, action: any) => ({
...state,
blockingByUri: {
...state.blockingByUri,
[action.data.uri]: true,
},
}),
[ACTIONS.COMMENT_MODERATION_UN_BLOCK_STARTED]: (state: CommentsState, action: any) => ({
...state,
unBlockingByUri: {
...state.unBlockingByUri,
[action.data.uri]: true,
},
}),
[ACTIONS.COMMENT_MODERATION_BLOCK_FAILED]: (state: CommentsState, action: any) => ({
...state,
blockingByUri: {
...state.blockingByUri,
[action.data.uri]: false,
},
}),
[ACTIONS.COMMENT_MODERATION_UN_BLOCK_FAILED]: (state: CommentsState, action: any) => ({
...state,
unBlockingByUri: {
...state.unBlockingByUri,
[action.data.uri]: false,
},
}),
[ACTIONS.COMMENT_MODERATION_BLOCK_COMPLETE]: (state: CommentsState, action: any) => {
const { channelUri } = action.data;
const commentById = Object.assign({}, state.commentById);
const blockingByUri = Object.assign({}, state.blockingByUri);
const moderationBlockList = state.moderationBlockList || [];
const newModerationBlockList = moderationBlockList.slice();
for (const commentId in commentById) {
const comment = commentById[commentId];
if (channelUri === comment.channel_url) {
delete commentById[comment.comment_id];
}
}
delete blockingByUri[channelUri];
newModerationBlockList.push(channelUri);
return {
...state,
commentById,
blockingByUri,
moderationBlockList: newModerationBlockList,
};
},
[ACTIONS.COMMENT_MODERATION_UN_BLOCK_COMPLETE]: (state: CommentsState, action: any) => {
const { channelUri } = action.data;
const unBlockingByUri = Object.assign(state.unBlockingByUri, {});
const moderationBlockList = state.moderationBlockList || [];
const newModerationBlockList = moderationBlockList.slice().filter((uri) => uri !== channelUri);
delete unBlockingByUri[channelUri];
return {
...state,
unBlockingByUri,
moderationBlockList: newModerationBlockList,
};
},
}, },
defaultState defaultState
); );

View file

@ -3,23 +3,11 @@ import { createSelector } from 'reselect';
const selectState = (state: { blocked: BlocklistState }) => state.blocked || {}; const selectState = (state: { blocked: BlocklistState }) => state.blocked || {};
export const selectBlockedChannels = createSelector(selectState, (state: BlocklistState) => { export const selectMutedChannels = createSelector(selectState, (state: BlocklistState) => {
return state.blockedChannels.filter(e => typeof e === 'string'); return state.blockedChannels.filter((e) => typeof e === 'string');
}); });
export const selectBlockedChannelsCount = createSelector(selectBlockedChannels, (state: Array<string>) => state.length); export const makeSelectChannelIsMuted = (uri: string) =>
createSelector(selectMutedChannels, (state: Array<string>) => {
export const selectBlockedChannelsObj = createSelector(selectState, (state: BlocklistState) => {
return state.blockedChannels.reduce((acc: any, val: any) => {
const outpoint = `${val.txid}:${String(val.nout)}`;
return {
...acc,
[outpoint]: 1,
};
}, {});
});
export const selectChannelIsBlocked = (uri: string) =>
createSelector(selectBlockedChannels, (state: Array<string>) => {
return state.includes(uri); return state.includes(uri);
}); });

View file

@ -1,22 +1,27 @@
// @flow // @flow
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { selectBlockedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc'; import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux'; import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux';
const selectState = state => state.comments || {}; const selectState = (state) => state.comments || {};
export const selectCommentsById = createSelector(selectState, state => state.commentById || {}); export const selectCommentsById = createSelector(selectState, (state) => state.commentById || {});
export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading);
export const selectIsFetchingComments = createSelector(selectState, state => state.isLoading); export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);
export const selectIsPostingComment = createSelector(selectState, state => state.isCommenting); export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId);
export const selectModerationBlockList = createSelector(selectState, (state) =>
export const selectIsFetchingReacts = createSelector(selectState, state => state.isFetchingReacts); state.moderationBlockList ? state.moderationBlockList.reverse() : []
);
export const selectOthersReactsById = createSelector(selectState, state => state.othersReactsByCommentId); export const selectBlockingByUri = createSelector(selectState, (state) => state.blockingByUri);
export const selectUnBlockingByUri = createSelector(selectState, (state) => state.unBlockingByUri);
export const selectFetchingModerationBlockList = createSelector(
selectState,
(state) => state.fetchingModerationBlockList
);
export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => { export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
const byClaimId = state.byId || {}; const byClaimId = state.byId || {};
@ -40,7 +45,7 @@ export const selectTopLevelCommentsByClaimId = createSelector(selectState, selec
const comments = {}; const comments = {};
// replace every comment_id in the list with the actual comment object // replace every comment_id in the list with the actual comment object
Object.keys(byClaimId).forEach(claimId => { Object.keys(byClaimId).forEach((claimId) => {
const commentIds = byClaimId[claimId]; const commentIds = byClaimId[claimId];
comments[claimId] = Array(commentIds === null ? 0 : commentIds.length); comments[claimId] = Array(commentIds === null ? 0 : commentIds.length);
@ -53,14 +58,14 @@ export const selectTopLevelCommentsByClaimId = createSelector(selectState, selec
}); });
export const makeSelectCommentForCommentId = (commentId: string) => export const makeSelectCommentForCommentId = (commentId: string) =>
createSelector(selectCommentsById, comments => comments[commentId]); createSelector(selectCommentsById, (comments) => comments[commentId]);
export const selectRepliesByParentId = createSelector(selectState, selectCommentsById, (state, byId) => { export const selectRepliesByParentId = createSelector(selectState, selectCommentsById, (state, byId) => {
const byParentId = state.repliesByParentId || {}; const byParentId = state.repliesByParentId || {};
const comments = {}; const comments = {};
// replace every comment_id in the list with the actual comment object // replace every comment_id in the list with the actual comment object
Object.keys(byParentId).forEach(id => { Object.keys(byParentId).forEach((id) => {
const commentIds = byParentId[id]; const commentIds = byParentId[id];
comments[id] = Array(commentIds === null ? 0 : commentIds.length); comments[id] = Array(commentIds === null ? 0 : commentIds.length);
@ -77,10 +82,10 @@ export const selectRepliesByParentId = createSelector(selectState, selectComment
selectState, selectState,
state => state.byId || {} state => state.byId || {}
); */ ); */
export const selectCommentsByUri = createSelector(selectState, state => { export const selectCommentsByUri = createSelector(selectState, (state) => {
const byUri = state.commentsByUri || {}; const byUri = state.commentsByUri || {};
const comments = {}; const comments = {};
Object.keys(byUri).forEach(uri => { Object.keys(byUri).forEach((uri) => {
const claimId = byUri[uri]; const claimId = byUri[uri];
if (claimId === null) { if (claimId === null) {
comments[uri] = null; comments[uri] = null;
@ -99,16 +104,16 @@ export const makeSelectCommentIdsForUri = (uri: string) =>
}); });
export const makeSelectMyReactionsForComment = (commentId: string) => export const makeSelectMyReactionsForComment = (commentId: string) =>
createSelector(selectState, state => { createSelector(selectState, (state) => {
return state.myReactsByCommentId[commentId] || []; return state.myReactsByCommentId[commentId] || [];
}); });
export const makeSelectOthersReactionsForComment = (commentId: string) => export const makeSelectOthersReactionsForComment = (commentId: string) =>
createSelector(selectState, state => { createSelector(selectState, (state) => {
return state.othersReactsByCommentId[commentId]; return state.othersReactsByCommentId[commentId];
}); });
export const selectPendingCommentReacts = createSelector(selectState, state => state.pendingCommentReactions); export const selectPendingCommentReacts = createSelector(selectState, (state) => state.pendingCommentReactions);
export const makeSelectCommentsForUri = (uri: string) => export const makeSelectCommentsForUri = (uri: string) =>
createSelector( createSelector(
@ -116,7 +121,7 @@ export const makeSelectCommentsForUri = (uri: string) =>
selectCommentsByUri, selectCommentsByUri,
selectClaimsById, selectClaimsById,
selectMyActiveClaims, selectMyActiveClaims,
selectBlockedChannels, selectMutedChannels,
selectBlacklistedOutpointMap, selectBlacklistedOutpointMap,
selectFilteredOutpointMap, selectFilteredOutpointMap,
makeSelectClientSetting(SETTINGS.SHOW_MATURE), makeSelectClientSetting(SETTINGS.SHOW_MATURE),
@ -125,7 +130,12 @@ export const makeSelectCommentsForUri = (uri: string) =>
const comments = byClaimId && byClaimId[claimId]; const comments = byClaimId && byClaimId[claimId];
return comments return comments
? comments.filter(comment => { ? comments.filter((comment) => {
if (!comment) {
// It may have been recently deleted after being blocked
return false;
}
const channelClaim = claimsById[comment.channel_id]; const channelClaim = claimsById[comment.channel_id];
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author // Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
@ -162,7 +172,7 @@ export const makeSelectTopLevelCommentsForUri = (uri: string) =>
selectCommentsByUri, selectCommentsByUri,
selectClaimsById, selectClaimsById,
selectMyActiveClaims, selectMyActiveClaims,
selectBlockedChannels, selectMutedChannels,
selectBlacklistedOutpointMap, selectBlacklistedOutpointMap,
selectFilteredOutpointMap, selectFilteredOutpointMap,
makeSelectClientSetting(SETTINGS.SHOW_MATURE), makeSelectClientSetting(SETTINGS.SHOW_MATURE),
@ -171,7 +181,7 @@ export const makeSelectTopLevelCommentsForUri = (uri: string) =>
const comments = byClaimId && byClaimId[claimId]; const comments = byClaimId && byClaimId[claimId];
return comments return comments
? comments.filter(comment => { ? comments.filter((comment) => {
if (!comment) { if (!comment) {
return false; return false;
} }
@ -212,7 +222,7 @@ export const makeSelectRepliesForParentId = (id: string) =>
selectCommentsById, selectCommentsById,
selectClaimsById, selectClaimsById,
selectMyActiveClaims, selectMyActiveClaims,
selectBlockedChannels, selectMutedChannels,
selectBlacklistedOutpointMap, selectBlacklistedOutpointMap,
selectFilteredOutpointMap, selectFilteredOutpointMap,
makeSelectClientSetting(SETTINGS.SHOW_MATURE), makeSelectClientSetting(SETTINGS.SHOW_MATURE),
@ -223,13 +233,13 @@ export const makeSelectRepliesForParentId = (id: string) =>
if (!replyIdsForParent.length) return null; if (!replyIdsForParent.length) return null;
const comments = []; const comments = [];
replyIdsForParent.forEach(cid => { replyIdsForParent.forEach((cid) => {
comments.push(commentsById[cid]); comments.push(commentsById[cid]);
}); });
// const comments = byParentId && byParentId[id]; // const comments = byParentId && byParentId[id];
return comments return comments
? comments.filter(comment => { ? comments.filter((comment) => {
if (!comment) { if (!comment) {
return false; return false;
} }
@ -265,6 +275,20 @@ export const makeSelectRepliesForParentId = (id: string) =>
); );
export const makeSelectTotalCommentsCountForUri = (uri: string) => export const makeSelectTotalCommentsCountForUri = (uri: string) =>
createSelector(makeSelectCommentsForUri(uri), comments => { createSelector(makeSelectCommentsForUri(uri), (comments) => {
return comments ? comments.length : 0; return comments ? comments.length : 0;
}); });
export const makeSelectChannelIsBlocked = (uri: string) =>
createSelector(selectModerationBlockList, (blockedChannelUris) => {
if (!blockedChannelUris || !blockedChannelUris) {
return false;
}
return blockedChannelUris.includes(uri);
});
export const makeSelectUriIsBlockingOrUnBlocking = (uri: string) =>
createSelector(selectBlockingByUri, selectUnBlockingByUri, (blockingByUri, unBlockingByUri) => {
return blockingByUri[uri] || unBlockingByUri[uri];
});

View file

@ -13,7 +13,7 @@ import {
makeSelectFileNameForUri, makeSelectFileNameForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search'; import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
import { selectBlockedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc'; import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import * as RENDER_MODES from 'constants/file_render_modes'; import * as RENDER_MODES from 'constants/file_render_modes';
@ -81,7 +81,7 @@ export const makeSelectNextUnplayedRecommended = (uri: string) =>
selectHistory, selectHistory,
selectClaimsByUri, selectClaimsByUri,
selectAllCostInfoByUri, selectAllCostInfoByUri,
selectBlockedChannels, selectMutedChannels,
( (
recommendedForUri: Array<string>, recommendedForUri: Array<string>,
history: Array<{ uri: string }>, history: Array<{ uri: string }>,

View file

@ -372,9 +372,9 @@ $metadata-z-index: 1;
.channel-staked__wrapper { .channel-staked__wrapper {
display: flex; display: flex;
position: absolute; position: absolute;
padding: 0.25rem; padding: 0.2rem;
bottom: -0.75rem; bottom: -0.75rem;
left: -0.75rem; left: -0.8rem;
background-color: var(--color-card-background); background-color: var(--color-card-background);
border-radius: 50%; border-radius: 50%;
} }
@ -390,6 +390,16 @@ $metadata-z-index: 1;
padding: 0; padding: 0;
} }
.channel-staked__wrapper--inline {
position: relative;
background-color: transparent;
display: inline-block;
bottom: auto;
left: auto;
top: var(--spacing-xs);
padding: 0;
}
.channel-staked__indicator { .channel-staked__indicator {
margin-left: 2px; margin-left: 2px;
z-index: 3; z-index: 3;

View file

@ -64,6 +64,12 @@
.claim-preview__wrapper { .claim-preview__wrapper {
padding: var(--spacing-m); padding: var(--spacing-m);
list-style: none; list-style: none;
&:hover {
.claim__menu-button {
display: block;
}
}
} }
.claim-preview__wrapper--notice { .claim-preview__wrapper--notice {
@ -247,17 +253,6 @@
.claim-preview-info { .claim-preview-info {
align-items: flex-start; align-items: flex-start;
.channel-thumbnail {
display: none;
@include handleChannelGif(1.4rem);
margin-right: 0;
margin-left: var(--spacing-s);
@media (min-width: $breakpoint-small) {
display: block;
}
}
} }
.claim-preview-info, .claim-preview-info,
@ -408,6 +403,10 @@
&:hover { &:hover {
cursor: pointer; cursor: pointer;
.claim__menu-button {
display: block;
}
} }
@media (min-width: $breakpoint-large) { @media (min-width: $breakpoint-large) {
@ -456,12 +455,23 @@
} }
.claim-tile__title { .claim-tile__title {
margin: var(--spacing-s); position: relative;
padding: var(--spacing-s);
padding-right: var(--spacing-m);
padding-bottom: 0;
margin-bottom: var(--spacing-s);
font-weight: 600; font-weight: 600;
color: var(--color-text); color: var(--color-text);
font-size: var(--font-small); font-size: var(--font-small);
min-height: 2rem; min-height: 2rem;
.claim__menu-button {
right: 0.2rem;
top: var(--spacing-s);
}
@media (min-width: $breakpoint-small) { @media (min-width: $breakpoint-small) {
min-height: 2.5rem; min-height: 2.5rem;
} }
@ -571,3 +581,36 @@
.claim-preview__null-label { .claim-preview__null-label {
margin: auto; margin: auto;
} }
.claim-preview__channel-staked {
display: flex;
align-items: center;
.channel-thumbnail {
@include handleChannelGif(1.4rem);
margin-right: 0;
}
}
.claim__menu-button {
position: absolute;
top: var(--spacing-xs);
right: var(--spacing-xs);
.icon {
stroke: var(--color-text);
}
&:not([aria-expanded='true']) {
display: none;
}
}
.claim__menu-button--inline {
position: relative;
display: block;
right: auto;
top: auto;
@extend .button--alt;
padding: 0 var(--spacing-xxs);
}

View file

@ -43,6 +43,15 @@
box-shadow: none; box-shadow: none;
} }
.menu__button {
&:hover {
.icon {
border-radius: var(--border-radius);
background-color: var(--color-card-background-highlighted);
}
}
}
.menu__title { .menu__title {
&[aria-expanded='true'] { &[aria-expanded='true'] {
background-color: var(--color-primary-alt); background-color: var(--color-primary-alt);
@ -54,16 +63,7 @@
animation: menu-animate-in var(--animation-duration) var(--animation-style); animation: menu-animate-in var(--animation-duration) var(--animation-style);
border-bottom-left-radius: var(--border-radius); border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius); border-bottom-right-radius: var(--border-radius);
}
.menu__list--header {
@extend .menu__list;
padding: var(--spacing-xs);
margin-top: 19px;
}
.menu__list--comments {
@extend .menu__list;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
padding: var(--spacing-xs) 0; padding: var(--spacing-xs) 0;
@ -73,6 +73,15 @@
} }
} }
.menu__list--header {
@extend .menu__list;
margin-top: 19px;
}
.menu__list--comments {
@extend .menu__list;
}
.menu__link { .menu__link {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -36,7 +36,7 @@
--color-button-primary-bg-hover: var(--color-primary-alt-2); --color-button-primary-bg-hover: var(--color-primary-alt-2);
--color-button-primary-hover-text: var(--color-primary-alt); --color-button-primary-hover-text: var(--color-primary-alt);
--color-button-secondary-bg: var(--color-secondary-alt); --color-button-secondary-bg: var(--color-secondary-alt);
--color-button-secondary-border: #c5d7ed; --color-button-secondary-border: var(--color-secondary-alt);
--color-button-secondary-text: var(--color-secondary); --color-button-secondary-text: var(--color-secondary);
--color-button-secondary-bg-hover: #b9d0e9; --color-button-secondary-bg-hover: #b9d0e9;
--color-button-alt-bg: var(--color-gray-1); --color-button-alt-bg: var(--color-gray-1);

View file

@ -262,6 +262,12 @@ textarea {
background-color: var(--color-help-warning-bg); background-color: var(--color-help-warning-bg);
color: var(--color-help-warning-text); color: var(--color-help-warning-text);
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
border: 1px solid var(--color-border);
}
.help--notice {
@extend .help--warning;
background-color: var(--color-card-background-highlighted);
} }
.help--inline { .help--inline {

View file

@ -24,9 +24,9 @@ function isNotFunction(object) {
} }
function createBulkThunkMiddleware() { function createBulkThunkMiddleware() {
return ({ dispatch, getState }) => next => action => { return ({ dispatch, getState }) => (next) => (action) => {
if (action.type === 'BATCH_ACTIONS') { if (action.type === 'BATCH_ACTIONS') {
action.actions.filter(isFunction).map(actionFn => actionFn(dispatch, getState)); action.actions.filter(isFunction).map((actionFn) => actionFn(dispatch, getState));
} }
return next(action); return next(action);
}; };
@ -68,7 +68,6 @@ const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
const blockedFilter = createFilter('blocked', ['blockedChannels']); const blockedFilter = createFilter('blocked', ['blockedChannels']);
const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']); const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']);
const whiteListedReducers = [ const whiteListedReducers = [
'comments',
'fileInfo', 'fileInfo',
'publish', 'publish',
'wallet', 'wallet',
@ -143,7 +142,7 @@ const sharedStateFilters = {
subscriptions: { subscriptions: {
source: 'subscriptions', source: 'subscriptions',
property: 'subscriptions', property: 'subscriptions',
transform: function(value) { transform: (value) => {
return value.map(({ uri }) => uri); return value.map(({ uri }) => uri);
}, },
}, },
@ -162,7 +161,7 @@ const sharedStateCb = ({ dispatch, getState }) => {
}; };
const populateAuthTokenHeader = () => { const populateAuthTokenHeader = () => {
return next => action => { return (next) => (action) => {
if ( if (
(action.type === ACTIONS.USER_FETCH_SUCCESS || action.type === ACTIONS.AUTHENTICATION_SUCCESS) && (action.type === ACTIONS.USER_FETCH_SUCCESS || action.type === ACTIONS.AUTHENTICATION_SUCCESS) &&
action.data.user.has_verified_email === true action.data.user.has_verified_email === true

View file

@ -1,8 +1,46 @@
// @flow // @flow
export function toHex(str: string): string { export function toHex(str: string): string {
var result = ''; const array = Array.from(str);
for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16); let result = '';
for (var i = 0; i < array.length; i++) {
const val = array[i];
const utf = toUTF8Array(val)
.map((num) => num.toString(16))
.join('');
result += utf;
} }
return result; return result;
} }
// https://gist.github.com/joni/3760795
// See comment that fixes an issue in the original gist
function toUTF8Array(str: string): Array<number> {
var utf8 = [];
for (var i = 0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
} else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f));
}
// surrogate pair
else {
i++;
charcode = (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)) + 0x010000;
utf8.push(
0xf0 | (charcode >> 18),
0x80 | ((charcode >> 12) & 0x3f),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f)
);
}
}
return utf8;
}