New moderation tools: block & mute #5572

Merged
neb-b merged 9 commits from block into master 2021-03-03 19:50:17 +01:00
75 changed files with 1115 additions and 877 deletions

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

@ -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,6 +208,21 @@ function ChannelPage(props: Props) {
<div className="channel-cover__gradient" /> <div className="channel-cover__gradient" />
</header> </header>
{isBlocked && !viewBlockedChannel ? (
<div className="main--empty">
<Yrbl
title={__('This channel is blocked')}
subtitle={__('Are you sure you want to view this content? Viewing will not unblock @%channel%', {
channel: channelName,
})}
actions={
<div className="section__actions">
<Button button="primary" label={__('View Content')} onClick={() => setViewBlockedChannel(true)} />
</div>
}
/>
</div>
) : (
<Tabs onChange={onTabChange} index={tabIndex}> <Tabs onChange={onTabChange} index={tabIndex}>
<TabList className="tabs__list--channel-page"> <TabList className="tabs__list--channel-page">
<Tab disabled={editing}>{__('Content')}</Tab> <Tab disabled={editing}>{__('Content')}</Tab>
@ -211,7 +231,11 @@ function ChannelPage(props: Props) {
</TabList> </TabList>
<TabPanels> <TabPanels>
<TabPanel> <TabPanel>
<ChannelContent uri={uri} channelIsBlackListed={channelIsBlackListed} /> <ChannelContent
uri={uri}
channelIsBlackListed={channelIsBlackListed}
viewBlockedChannel={viewBlockedChannel}
/>
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<ChannelAbout uri={uri} /> <ChannelAbout uri={uri} />
@ -221,6 +245,7 @@ function ChannelPage(props: Props) {
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
</Tabs> </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 = {}; },
if (activeChannelClaim) {
try {
channelSignature = await Lbry.channel_sign({
channel_id: activeChannelClaim.claim_id,
hexdata: toHex(activeChannelClaim.name),
}); });
const creatorIdForAction = claim ? claim.claim_id : null;
const creatorNameForAction = claim ? claim.name : null;
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) {} } 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
[ACTIONS.COMMENT_HIDE_FAILED]: (state: CommentsState, action: any) => ({
...state, ...state,
isLoading: false, moderationBlockList: blockList,
fetchingModerationBlockList: false,
};
},
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_FAILED]: (state: CommentsState, action: any) => ({
...state,
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;
}