New moderation tools: block & mute ()

* initial support for block/mute

* hide blocked + muted content everywhere

* add info message for blocked/muted characteristics

* sort blocked list by most recent block first

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

* cleanup

* delete unused files

* always pass mute/block list to claim_search on homepage

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

View file

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

View file

@ -6,6 +6,8 @@ const Comments = {
enabled: Boolean(COMMENT_SERVER_API),
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_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
};
@ -30,8 +32,8 @@ function fetchCommentsApi(method: string, params: {}) {
};
return fetch(url, options)
.then(res => res.json())
.then(res => res.result);
.then((res) => res.json())
.then((res) => res.result);
}
export default Comments;

View file

@ -1,14 +1,6 @@
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';
const select = (state, props) => ({
blockedChannelUris: selectBlockedChannels(state),
});
const select = (state, props) => ({});
export default connect(select, {
doChannelUnsubscribe,
doOpenModal,
})(AbandonedChannelPreview);
export default connect(select)(AbandonedChannelPreview);

View file

@ -2,28 +2,18 @@
import React from 'react';
import classnames from 'classnames';
import ChannelThumbnail from 'component/channelThumbnail';
import Button from 'component/button';
import { parseURI } from 'lbry-redux';
import * as ICONS from '../../constants/icons';
import * as MODALS from 'constants/modal_types';
type SubscriptionArgs = {
channelName: string,
uri: string,
};
import ChannelBlockButton from 'component/channelBlockButton';
import ChannelMuteButton from 'component/channelMuteButton';
type Props = {
uri: string,
doChannelUnsubscribe: SubscriptionArgs => void,
type: string,
blockedChannelUris: Array<string>,
doOpenModal: (string, {}) => void,
};
function AbandonedChannelPreview(props: Props) {
const { uri, doChannelUnsubscribe, type, blockedChannelUris, doOpenModal } = props;
const { uri, type } = props;
const { channelName } = parseURI(uri);
const isBlockedChannel = blockedChannelUris.includes(uri);
return (
<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>
<div className="claim-preview__actions">
{isBlockedChannel && (
<Button
iconColor="red"
icon={ICONS.UNBLOCK}
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 className="section__actions">
<ChannelBlockButton uri={uri} />
<ChannelMuteButton uri={uri} />
</div>
</div>
</div>
</div>

View file

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

View file

@ -56,14 +56,14 @@ type Props = {
goForward: () => void,
index: number,
length: number,
push: string => void,
push: (string) => void,
},
fetchAccessToken: () => void,
fetchChannelListMine: () => void,
signIn: () => void,
requestDownloadUpgrade: () => void,
onSignedIn: () => void,
setLanguage: string => void,
setLanguage: (string) => void,
isUpgradeAvailable: boolean,
autoUpdateDownloaded: boolean,
updatePreferences: () => Promise<any>,
@ -83,7 +83,8 @@ type Props = {
activeChannelClaim: ?ChannelClaim,
myChannelUrls: ?Array<string>,
setActiveChannelIfNotSet: () => void,
setIncognito: boolean => void,
setIncognito: (boolean) => void,
fetchModBlockedList: () => void,
};
function App(props: Props) {
@ -114,6 +115,7 @@ function App(props: Props) {
activeChannelClaim,
setActiveChannelIfNotSet,
setIncognito,
fetchModBlockedList,
} = props;
const appRef = useRef();
@ -134,7 +136,7 @@ function App(props: Props) {
const showUpgradeButton =
(autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && !upgradeNagClosed;
// 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 rawReferrerParam = urlParams.get('r');
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
@ -166,7 +168,7 @@ function App(props: Props) {
useEffect(() => {
if (!uploadCount) return;
const handleBeforeUnload = event => {
const handleBeforeUnload = (event) => {
event.preventDefault();
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
useEffect(() => {
const handleForwardAndBackButtons = e => {
const handleForwardAndBackButtons = (e) => {
switch (e.button) {
case MOUSE_BACK_BTN:
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
useEffect(() => {
const handleKeyPress = e => {
const handleKeyPress = (e) => {
if (e.key === ' ' && e.target === document.body) {
e.preventDefault();
}
@ -244,6 +246,10 @@ function App(props: Props) {
} else if (hasNoChannels) {
setIncognito(true);
}
if (hasMyChannels) {
fetchModBlockedList();
}
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]);
useEffect(() => {
@ -358,7 +364,7 @@ function App(props: Props) {
[`${MAIN_WRAPPER_CLASS}--scrollbar`]: useCustomScrollbar,
})}
ref={appRef}
onContextMenu={IS_WEB ? undefined : e => openContextMenu(e)}
onContextMenu={IS_WEB ? undefined : (e) => openContextMenu(e)}
>
{IS_WEB && lbryTvApiStatus === STATUS_DOWN ? (
<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,
SETTINGS,
} from 'lbry-redux';
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { withRouter } from 'react-router';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { makeSelectClientSetting } from 'redux/selectors/settings';
@ -24,7 +24,7 @@ const select = (state, props) => {
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
totalPages: makeSelectTotalPagesInChannelSearch(props.uri, PAGE_SIZE)(state),
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
channelIsBlocked: selectChannelIsBlocked(props.uri)(state),
channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
claim: props.uri && makeSelectClaimForUri(props.uri)(state),
isAuthenticated: selectUserVerifiedEmail(state),
showMature: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),

View file

@ -29,6 +29,7 @@ type Props = {
isAuthenticated: boolean,
showMature: boolean,
tileLayout: boolean,
viewBlockedChannel: boolean,
};
function ChannelContent(props: Props) {
@ -44,6 +45,7 @@ function ChannelContent(props: Props) {
defaultInfiniteScroll = true,
showMature,
tileLayout,
viewBlockedChannel,
} = props;
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
const [searchQuery, setSearchQuery] = React.useState('');
@ -120,6 +122,7 @@ function ChannelContent(props: Props) {
{claim && claimsInChannel > 0 ? (
<ClaimListDiscover
showHiddenByUser={viewBlockedChannel}
forceShowReposts
tileLayout={tileLayout}
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,
} from 'lbry-redux';
import { doOpenModal } from 'redux/actions/app';
import { doUpdateBlockListForPublishedChannel } from 'redux/actions/comments';
import ChannelPage from './view';
const select = (state, props) => ({
@ -38,12 +38,16 @@ const select = (state, props) => ({
balance: selectBalance(state),
});
const perform = dispatch => ({
const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
updateChannel: params => dispatch(doUpdateChannel(params)),
createChannel: params => {
updateChannel: (params) => dispatch(doUpdateChannel(params)),
createChannel: (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()),
});

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

View file

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

View file

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

View file

@ -20,17 +20,18 @@ type Props = {
doClaimSearch: ({}) => void,
loading: boolean,
personalView: boolean,
doToggleTagFollowDesktop: string => void,
doToggleTagFollowDesktop: (string) => void,
meta?: Node,
showNsfw: 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 },
claimSearchByQuery: {
[string]: Array<string>,
},
claimSearchByQueryLastPageReached: { [string]: boolean },
hiddenUris: Array<string>,
mutedUris: Array<string>,
blockedUris: Array<string>,
hiddenNsfwMessage?: Node,
channelIds?: Array<string>,
claimIds?: Array<string>,
@ -43,13 +44,12 @@ type Props = {
header?: Node,
headerLabel?: string | Node,
name?: string,
hideBlock?: boolean,
hideAdvancedFilter?: boolean,
claimType?: Array<string>,
defaultClaimType?: Array<string>,
streamType?: string | Array<string>,
defaultStreamType?: string | Array<string>,
renderProperties?: Claim => Node,
renderProperties?: (Claim) => Node,
includeSupportAction?: boolean,
repostedClaimId?: string,
pageSize?: number,
@ -64,6 +64,7 @@ type Props = {
languageSetting: string,
searchInLanguage: boolean,
scrollAnchor?: string,
showHiddenByUser?: boolean,
};
function ClaimListDiscover(props: Props) {
@ -80,7 +81,8 @@ function ClaimListDiscover(props: Props) {
hideReposts,
history,
location,
hiddenUris,
mutedUris,
blockedUris,
hiddenNsfwMessage,
defaultOrderBy,
orderBy,
@ -89,7 +91,6 @@ function ClaimListDiscover(props: Props) {
name,
claimType,
pageSize,
hideBlock,
defaultClaimType,
streamType,
defaultStreamType,
@ -112,6 +113,7 @@ function ClaimListDiscover(props: Props) {
languageSetting,
searchInLanguage,
scrollAnchor,
showHiddenByUser = false,
} = props;
const didNavigateForward = history.action === 'PUSH';
const { search } = location;
@ -120,13 +122,14 @@ function ClaimListDiscover(props: Props) {
const isLargeScreen = useIsLargeScreen();
const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${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 tagsParam = // can be 'x,y,z' or 'x' or ['x','y'] or CS.CONSTANT
(tags && getParamFromTags(tags)) ||
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
(defaultTags && getParamFromTags(defaultTags));
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 languageParams = searchInLanguage
@ -206,7 +209,7 @@ function ClaimListDiscover(props: Props) {
no_totals: true,
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 : [],
order_by:
orderParam === CS.ORDER_BY_TRENDING
@ -247,12 +250,7 @@ function ClaimListDiscover(props: Props) {
if (claimType !== CS.CLAIM_CHANNEL) {
if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
options.release_time = `>${Math.floor(
moment()
.subtract(1, freshnessParam)
.startOf('hour')
.unix()
)}`;
options.release_time = `>${Math.floor(moment().subtract(1, freshnessParam).startOf('hour').unix())}`;
} else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
// Warning - hack below
// 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.any_tags && options.any_tags.length > 20)
) {
options.release_time = `>${Math.floor(
moment()
.subtract(3, CS.FRESH_MONTH)
.startOf('week')
.unix()
)}`;
options.release_time = `>${Math.floor(moment().subtract(3, CS.FRESH_MONTH).startOf('week').unix())}`;
} else if (
(options.channel_ids && options.channel_ids.length > 10) ||
(options.any_tags && options.any_tags.length > 10)
) {
options.release_time = `>${Math.floor(
moment()
.subtract(1, CS.FRESH_YEAR)
.startOf('week')
.unix()
)}`;
options.release_time = `>${Math.floor(moment().subtract(1, CS.FRESH_YEAR).startOf('week').unix())}`;
} else {
// Hack for at least the New page until https://github.com/lbryio/lbry-sdk/issues/2591 is fixed
options.release_time = `<${Math.floor(
moment()
.startOf('minute')
.unix()
)}`;
options.release_time = `<${Math.floor(moment().startOf('minute').unix())}`;
}
}
}
@ -333,14 +317,14 @@ function ClaimListDiscover(props: Props) {
if (hideReposts && !options.reposted_claim_id && !forceShowReposts) {
if (Array.isArray(options.claim_type)) {
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 {
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 claimSearchResult = claimSearchByQuery[claimSearchCacheQuery];
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery];
@ -499,8 +483,8 @@ function ClaimListDiscover(props: Props) {
timedOutMessage={timedOutMessage}
renderProperties={renderProperties}
includeSupportAction={includeSupportAction}
hideBlock={hideBlock}
injectedItem={injectedItem}
showHiddenByUser={showHiddenByUser}
/>
{loading && (
<div className="claim-grid">
@ -527,8 +511,8 @@ function ClaimListDiscover(props: Props) {
timedOutMessage={timedOutMessage}
renderProperties={renderProperties}
includeSupportAction={includeSupportAction}
hideBlock={hideBlock}
injectedItem={injectedItem}
showHiddenByUser={showHiddenByUser}
/>
{loading && new Array(dynamicPageSize).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
</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,
makeSelectStreamingUrlForUri,
} from 'lbry-redux';
import { selectBlockedChannels, selectChannelIsBlocked } from 'redux/selectors/blocked';
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { selectModerationBlockList } from 'redux/selectors/comments';
import ClaimPreview from './view';
const select = (state, props) => ({
@ -30,17 +31,18 @@ const select = (state, props) => ({
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state),
filteredOutpoints: selectFilteredOutpoints(state),
blockedChannelUris: selectBlockedChannels(state),
mutedUris: selectMutedChannels(state),
blockedUris: selectModerationBlockList(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),
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
getFile: uri => dispatch(doFileGet(uri, false)),
const perform = (dispatch) => ({
resolveUri: (uri) => dispatch(doResolveUri(uri)),
getFile: (uri) => dispatch(doFileGet(uri, false)),
});
export default connect(select, perform)(ClaimPreview);

View file

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

View file

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

View file

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

View file

@ -1,16 +1,18 @@
import { connect } from 'react-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 { makeSelectClientSetting } from 'redux/selectors/settings';
import ClaimListDiscover from './view';
const select = state => ({
const select = (state) => ({
claimSearchByQuery: selectClaimSearchByQuery(state),
fetchingClaimSearchByQuery: selectFetchingClaimSearchByQuery(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
hiddenUris: selectBlockedChannels(state),
mutedUris: selectMutedChannels(state),
blockedUris: selectModerationBlockList(state),
});
const perform = {

View file

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

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { makeSelectThumbnailForUri, selectMyChannelClaims } from 'lbry-redux';
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 { doSetPlayingUri } from 'redux/actions/content';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
@ -11,7 +11,7 @@ import Comment from './view';
const select = (state, props) => ({
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,
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
activeChannelClaim: selectActiveChannelClaim(state),

View file

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

View file

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

View file

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

View file

@ -238,6 +238,13 @@ export const icons = {
<circle cx="12" cy="12" r="10" />
</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(
<g>
<circle cx="12" cy="12" r="5" />

View file

@ -10,7 +10,7 @@ function FileAuthor(props: Props) {
const { channelUri } = props;
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>
);

View file

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

View file

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

View file

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

View file

@ -272,7 +272,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<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.CHANNELS}`} component={ChannelsPage} />
<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_COMPLETED = 'COMMENT_PIN_COMPLETED';
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
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';

View file

@ -82,6 +82,7 @@ export const LIBRARY = 'Folder';
export const TAG = 'Tag';
export const SUPPORT = 'TrendingUp';
export const BLOCK = 'Slash';
export const MUTE = 'VolumeX';
export const UNBLOCK = 'Circle';
export const VIEW = 'View';
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 CONFIRM_AGE = 'confirm_age';
export const SYNC_ENABLE = 'SYNC_ENABLE';
export const REMOVE_BLOCKED = 'remove_blocked';
export const IMAGE_UPLOAD = 'image_upload';
export const MOBILE_SEARCH = 'mobile_search';
export const VIEW_IMAGE = 'view_image';

View file

@ -24,6 +24,7 @@ exports.SEND = 'send';
exports.SETTINGS = 'settings';
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
exports.SETTINGS_ADVANCED = 'settings/advanced';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
exports.SHOW = 'show';
exports.ACCOUNT = 'account';
exports.SEARCH = 'search';
@ -36,7 +37,6 @@ exports.CHANNELS_FOLLOWING = 'following';
exports.DEPRECATED__CHANNELS_FOLLOWING_MANAGE = 'following/channels/manage';
exports.CHANNELS_FOLLOWING_DISCOVER = 'following/discover';
exports.WALLET = 'wallet';
exports.BLOCKED = 'blocked';
exports.CHANNELS = 'channels';
exports.EMBED = 'embed';
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 ModalFirstSubscription from 'modal/modalFirstSubscription';
import ModalConfirmTransaction from 'modal/modalConfirmTransaction';
import ModalRemoveBlocked from 'modal/modalRemoveBlocked';
import ModalSocialShare from 'modal/modalSocialShare';
import ModalSendTip from 'modal/modalSendTip';
import ModalPublish from 'modal/modalPublish';
@ -32,7 +31,6 @@ import ModalCommentAcknowledgement from 'modal/modalCommentAcknowledgement';
import ModalWalletSend from 'modal/modalWalletSend';
import ModalWalletReceive from 'modal/modalWalletReceive';
import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome';
import ModalCreateChannel from 'modal/modalChannelCreate';
import ModalSetReferrer from 'modal/modalSetReferrer';
import ModalSignOut from 'modal/modalSignOut';
import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate';
@ -130,8 +128,6 @@ function ModalRouter(props: Props) {
return <ModalWalletReceive {...modalProps} />;
case MODALS.YOUTUBE_WELCOME:
return <ModalYoutubeWelcome />;
case MODALS.CREATE_CHANNEL:
return <ModalCreateChannel {...modalProps} />;
case MODALS.SET_REFERRER:
return <ModalSetReferrer {...modalProps} />;
case MODALS.SIGN_OUT:
@ -142,8 +138,6 @@ function ModalRouter(props: Props) {
return <ModalFileSelection {...modalProps} />;
case MODALS.LIQUIDATE_SUPPORTS:
return <ModalSupportsLiquidate {...modalProps} />;
case MODALS.REMOVE_BLOCKED:
return <ModalRemoveBlocked {...modalProps} />;
case MODALS.IMAGE_UPLOAD:
return <ModalImageUpload {...modalProps} />;
case MODALS.SYNC_ENABLE:

View file

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

View file

@ -6,7 +6,6 @@ import { parseURI } from 'lbry-redux';
import { YOUTUBE_STATUSES } from 'lbryinc';
import Page from 'component/page';
import SubscribeButton from 'component/subscribeButton';
import BlockButton from 'component/blockButton';
import ShareButton from 'component/shareButton';
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
import { useHistory } from 'react-router';
@ -21,6 +20,8 @@ import classnames from 'classnames';
import HelpLink from 'component/common/help-link';
import ClaimSupportButton from 'component/claimSupportButton';
import ChannelStakedIndicator from 'component/channelStakedIndicator';
import ClaimMenuList from 'component/claimMenuList';
import Yrbl from 'component/yrbl';
export const PAGE_VIEW_QUERY = `view`;
const ABOUT_PAGE = `about`;
@ -46,6 +47,7 @@ type Props = {
subCount: number,
pending: boolean,
youtubeChannels: ?Array<{ channel_claim_id: string, sync_status: string, transfer_state: string }>,
blockedChannels: Array<string>,
};
function ChannelPage(props: Props) {
@ -63,12 +65,14 @@ function ChannelPage(props: Props) {
subCount,
pending,
youtubeChannels,
blockedChannels,
} = props;
const {
push,
goBack,
location: { search },
} = useHistory();
const [viewBlockedChannel, setViewBlockedChannel] = React.useState(false);
const urlParams = new URLSearchParams(search);
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
const [discussionWasMounted, setDiscussionWasMounted] = React.useState(false);
@ -77,6 +81,7 @@ function ChannelPage(props: Props) {
const { permanent_url: permanentUrl } = claim;
const claimId = claim.claim_id;
const formattedSubCount = Number(subCount).toLocaleString();
const isBlocked = claim && blockedChannels.includes(claim.permanent_url);
const isMyYouTubeChannel =
claim &&
youtubeChannels &&
@ -157,7 +162,7 @@ function ChannelPage(props: Props) {
{!channelIsBlocked && !channelIsBlackListed && <ShareButton uri={uri} />}
{!channelIsBlocked && <ClaimSupportButton uri={uri} />}
{!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}
{!isSubscribed && <BlockButton uri={permanentUrl} />}
<ClaimMenuList uri={claim.permanent_url} inline />
</div>
{cover && (
<img
@ -203,6 +208,21 @@ function ChannelPage(props: Props) {
<div className="channel-cover__gradient" />
</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}>
<TabList className="tabs__list--channel-page">
<Tab disabled={editing}>{__('Content')}</Tab>
@ -211,7 +231,11 @@ function ChannelPage(props: Props) {
</TabList>
<TabPanels>
<TabPanel>
<ChannelContent uri={uri} channelIsBlackListed={channelIsBlackListed} />
<ChannelContent
uri={uri}
channelIsBlackListed={channelIsBlackListed}
viewBlockedChannel={viewBlockedChannel}
/>
</TabPanel>
<TabPanel>
<ChannelAbout uri={uri} />
@ -221,6 +245,7 @@ function ChannelPage(props: Props) {
</TabPanel>
</TabPanels>
</Tabs>
)}
</Page>
);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,12 @@
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';
const select = state => ({
uris: selectBlockedChannels(state),
const select = (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
import * as ICONS from 'constants/icons';
import React from 'react';
import classnames from 'classnames';
import ClaimList from 'component/claimList';
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 = {
uris: Array<string>,
mutedUris: ?Array<string>,
blockedUris: ?Array<string>,
fetchingModerationBlockList: boolean,
};
const VIEW_BLOCKED = 'blocked';
const VIEW_MUTED = 'muted';
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 (
<Page>
{uris && uris.length ? (
<Card
isBodyList
title={__('Your blocked channels')}
body={<ClaimList uris={uris} showUnresolvedClaims showHiddenByUser />}
/>
) : (
{loading && (
<div className="main--empty">
<section className="card card--section">
<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>
<Spinner />
</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>
);
}

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import * as ACTIONS from 'constants/action_types';
import { selectPrefsReady } from 'redux/selectors/sync';
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 ready = selectPrefsReady(state);

View file

@ -1,13 +1,14 @@
// @flow
import * as ACTIONS from 'constants/action_types';
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 {
makeSelectCommentIdsForUri,
makeSelectMyReactionsForComment,
makeSelectOthersReactionsForComment,
selectPendingCommentReacts,
selectModerationBlockList,
} from 'redux/selectors/comments';
import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications';
import { selectActiveChannelClaim } from 'redux/selectors/app';
@ -50,7 +51,7 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
});
return result;
})
.catch(error => {
.catch((error) => {
dispatch({
type: ACTIONS.COMMENT_LIST_FAILED,
data: error,
@ -89,7 +90,7 @@ export function doCommentReactList(uri: string | null, commentId?: string) {
},
});
})
.catch(error => {
.catch((error) => {
dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
data: error,
@ -171,14 +172,14 @@ export function doCommentReact(commentId: string, type: string) {
data: commentId + type,
});
})
.catch(error => {
.catch((error) => {
dispatch({
type: ACTIONS.COMMENT_REACT_FAILED,
data: commentId + type,
});
const myRevertedReactsObj = myReacts
.filter(el => el !== type)
.filter((el) => el !== type)
.reduce((acc, el) => {
acc[el] = 1;
return acc;
@ -233,14 +234,20 @@ export function doCommentCreate(comment: string = '', claim_id: string = '', par
});
return result;
})
.catch(error => {
.catch((error) => {
dispatch({
type: ACTIONS.COMMENT_CREATE_FAILED,
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(
doToast({
message: 'Unable to create comment, please try again later.',
message: toastMessage,
isError: true,
})
);
@ -274,7 +281,7 @@ export function doCommentPin(commentId: string, remove: boolean) {
data: result,
});
})
.catch(error => {
.catch((error) => {
dispatch({
type: ACTIONS.COMMENT_PIN_FAILED,
data: error,
@ -339,7 +346,7 @@ export function doCommentAbandon(commentId: string, creatorChannelUri?: string)
);
}
})
.catch(error => {
.catch((error) => {
dispatch({
type: ACTIONS.COMMENT_ABANDON_FAILED,
data: error,
@ -389,7 +396,7 @@ export function doCommentUpdate(comment_id: string, comment: string) {
);
}
})
.catch(error => {
.catch((error) => {
dispatch({
type: ACTIONS.COMMENT_UPDATE_FAILED,
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
export function doCommentModBlock(commentAuthor: string) {
export function doCommentModToggleBlock(channelUri: string, unblock: boolean = false) {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const claim = selectClaimsByUri(state)[commentAuthor];
const myChannels = selectMyChannelClaims(state);
const claim = selectClaimsByUri(state)[channelUri];
if (!claim) {
console.error("Can't find claim to block"); // eslint-disable-line
return;
}
const creatorIdToBan = claim ? claim.claim_id : null;
const creatorNameToBan = claim ? claim.name : null;
const activeChannelClaim = selectActiveChannelClaim(state);
let channelSignature = {};
if (activeChannelClaim) {
try {
channelSignature = await Lbry.channel_sign({
channel_id: activeChannelClaim.claim_id,
hexdata: toHex(activeChannelClaim.name),
dispatch({
type: unblock ? ACTIONS.COMMENT_MODERATION_UN_BLOCK_STARTED : ACTIONS.COMMENT_MODERATION_BLOCK_STARTED,
data: {
uri: channelUri,
},
});
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) {}
}
}
return Comments.moderation_block({
mod_channel_id: activeChannelClaim.claim_id,
mod_channel_name: activeChannelClaim.name,
signature: channelSignature.signature,
signing_ts: channelSignature.signing_ts,
banned_channel_id: creatorIdToBan,
banned_channel_name: creatorNameToBan,
delete_all: true,
const sharedModBlockParams = unblock
? {
un_blocked_channel_id: creatorIdForAction,
un_blocked_channel_name: creatorNameForAction,
}
: {
blocked_channel_id: creatorIdForAction,
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();
if (newBlockedChannels.includes(uri)) {
newBlockedChannels = newBlockedChannels.filter(id => id !== uri);
newBlockedChannels = newBlockedChannels.filter((id) => id !== uri);
} else {
newBlockedChannels.push(uri);
newBlockedChannels.unshift(uri);
}
return {
@ -29,7 +29,7 @@ export default handleActions(
action: { data: { blocked: ?Array<string> } }
) => {
const { blocked } = action.data;
const sanitizedBlocked = blocked && blocked.filter(e => typeof e === 'string');
const sanitizedBlocked = blocked && blocked.filter((e) => typeof e === 'string');
return {
...state,
blockedChannels: sanitizedBlocked && sanitizedBlocked.length ? sanitizedBlocked : state.blockedChannels,

View file

@ -16,6 +16,10 @@ const defaultState: CommentsState = {
typesReacting: [],
myReactsByCommentId: undefined,
othersReactsByCommentId: undefined,
moderationBlockList: undefined,
fetchingModerationBlockList: false,
blockingByUri: {},
unBlockingByUri: {},
};
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) => {
const { comments, claimId, uri } = action.data;
@ -227,17 +231,15 @@ export default handleActions(
isLoading: false,
};
},
// do nothing
[ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({
...state,
isCommenting: false,
}),
// do nothing
[ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({
...state,
isCommenting: true,
}),
// replace existing comment with comment returned here under its comment_id
[ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => {
const { comment } = action.data;
const commentById = Object.assign({}, state.commentById);
@ -249,25 +251,100 @@ export default handleActions(
isCommenting: false,
};
},
// nothing can be done here
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
...state,
isCmmenting: false,
}),
// nothing can really be done here
[ACTIONS.COMMENT_HIDE_STARTED]: (state: CommentsState, action: any) => ({
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_STARTED]: (state: CommentsState, action: any) => ({
...state,
isLoading: true,
fetchingModerationBlockList: true,
}),
[ACTIONS.COMMENT_HIDE_COMPLETED]: (state: CommentsState, action: any) => ({
...state, // todo: add HiddenComments state & create selectors
isLoading: false,
}),
// nothing can be done here
[ACTIONS.COMMENT_HIDE_FAILED]: (state: CommentsState, action: any) => ({
[ACTIONS.COMMENT_MODERATION_BLOCK_LIST_COMPLETED]: (state: CommentsState, action: any) => {
const { blockList } = action.data;
return {
...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
);

View file

@ -3,23 +3,11 @@ import { createSelector } from 'reselect';
const selectState = (state: { blocked: BlocklistState }) => state.blocked || {};
export const selectBlockedChannels = createSelector(selectState, (state: BlocklistState) => {
return state.blockedChannels.filter(e => typeof e === 'string');
export const selectMutedChannels = createSelector(selectState, (state: BlocklistState) => {
return state.blockedChannels.filter((e) => typeof e === 'string');
});
export const selectBlockedChannelsCount = createSelector(selectBlockedChannels, (state: Array<string>) => state.length);
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>) => {
export const makeSelectChannelIsMuted = (uri: string) =>
createSelector(selectMutedChannels, (state: Array<string>) => {
return state.includes(uri);
});

View file

@ -1,22 +1,27 @@
// @flow
import * as SETTINGS from 'constants/settings';
import { createSelector } from 'reselect';
import { selectBlockedChannels } from 'redux/selectors/blocked';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
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 selectIsFetchingComments = createSelector(selectState, state => state.isLoading);
export const selectIsPostingComment = createSelector(selectState, state => state.isCommenting);
export const selectIsFetchingReacts = createSelector(selectState, state => state.isFetchingReacts);
export const selectOthersReactsById = createSelector(selectState, state => state.othersReactsByCommentId);
export const selectCommentsById = createSelector(selectState, (state) => state.commentById || {});
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 selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId);
export const selectModerationBlockList = createSelector(selectState, (state) =>
state.moderationBlockList ? state.moderationBlockList.reverse() : []
);
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) => {
const byClaimId = state.byId || {};
@ -40,7 +45,7 @@ export const selectTopLevelCommentsByClaimId = createSelector(selectState, selec
const comments = {};
// 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];
comments[claimId] = Array(commentIds === null ? 0 : commentIds.length);
@ -53,14 +58,14 @@ export const selectTopLevelCommentsByClaimId = createSelector(selectState, selec
});
export const makeSelectCommentForCommentId = (commentId: string) =>
createSelector(selectCommentsById, comments => comments[commentId]);
createSelector(selectCommentsById, (comments) => comments[commentId]);
export const selectRepliesByParentId = createSelector(selectState, selectCommentsById, (state, byId) => {
const byParentId = state.repliesByParentId || {};
const comments = {};
// 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];
comments[id] = Array(commentIds === null ? 0 : commentIds.length);
@ -77,10 +82,10 @@ export const selectRepliesByParentId = createSelector(selectState, selectComment
selectState,
state => state.byId || {}
); */
export const selectCommentsByUri = createSelector(selectState, state => {
export const selectCommentsByUri = createSelector(selectState, (state) => {
const byUri = state.commentsByUri || {};
const comments = {};
Object.keys(byUri).forEach(uri => {
Object.keys(byUri).forEach((uri) => {
const claimId = byUri[uri];
if (claimId === null) {
comments[uri] = null;
@ -99,16 +104,16 @@ export const makeSelectCommentIdsForUri = (uri: string) =>
});
export const makeSelectMyReactionsForComment = (commentId: string) =>
createSelector(selectState, state => {
createSelector(selectState, (state) => {
return state.myReactsByCommentId[commentId] || [];
});
export const makeSelectOthersReactionsForComment = (commentId: string) =>
createSelector(selectState, state => {
createSelector(selectState, (state) => {
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) =>
createSelector(
@ -116,7 +121,7 @@ export const makeSelectCommentsForUri = (uri: string) =>
selectCommentsByUri,
selectClaimsById,
selectMyActiveClaims,
selectBlockedChannels,
selectMutedChannels,
selectBlacklistedOutpointMap,
selectFilteredOutpointMap,
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
@ -125,7 +130,12 @@ export const makeSelectCommentsForUri = (uri: string) =>
const comments = byClaimId && byClaimId[claimId];
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];
// 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,
selectClaimsById,
selectMyActiveClaims,
selectBlockedChannels,
selectMutedChannels,
selectBlacklistedOutpointMap,
selectFilteredOutpointMap,
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
@ -171,7 +181,7 @@ export const makeSelectTopLevelCommentsForUri = (uri: string) =>
const comments = byClaimId && byClaimId[claimId];
return comments
? comments.filter(comment => {
? comments.filter((comment) => {
if (!comment) {
return false;
}
@ -212,7 +222,7 @@ export const makeSelectRepliesForParentId = (id: string) =>
selectCommentsById,
selectClaimsById,
selectMyActiveClaims,
selectBlockedChannels,
selectMutedChannels,
selectBlacklistedOutpointMap,
selectFilteredOutpointMap,
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
@ -223,13 +233,13 @@ export const makeSelectRepliesForParentId = (id: string) =>
if (!replyIdsForParent.length) return null;
const comments = [];
replyIdsForParent.forEach(cid => {
replyIdsForParent.forEach((cid) => {
comments.push(commentsById[cid]);
});
// const comments = byParentId && byParentId[id];
return comments
? comments.filter(comment => {
? comments.filter((comment) => {
if (!comment) {
return false;
}
@ -265,6 +275,20 @@ export const makeSelectRepliesForParentId = (id: string) =>
);
export const makeSelectTotalCommentsCountForUri = (uri: string) =>
createSelector(makeSelectCommentsForUri(uri), comments => {
createSelector(makeSelectCommentsForUri(uri), (comments) => {
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,
} from 'lbry-redux';
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
import { selectBlockedChannels } from 'redux/selectors/blocked';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import * as RENDER_MODES from 'constants/file_render_modes';
@ -81,7 +81,7 @@ export const makeSelectNextUnplayedRecommended = (uri: string) =>
selectHistory,
selectClaimsByUri,
selectAllCostInfoByUri,
selectBlockedChannels,
selectMutedChannels,
(
recommendedForUri: Array<string>,
history: Array<{ uri: string }>,

View file

@ -372,9 +372,9 @@ $metadata-z-index: 1;
.channel-staked__wrapper {
display: flex;
position: absolute;
padding: 0.25rem;
padding: 0.2rem;
bottom: -0.75rem;
left: -0.75rem;
left: -0.8rem;
background-color: var(--color-card-background);
border-radius: 50%;
}
@ -390,6 +390,16 @@ $metadata-z-index: 1;
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 {
margin-left: 2px;
z-index: 3;

View file

@ -64,6 +64,12 @@
.claim-preview__wrapper {
padding: var(--spacing-m);
list-style: none;
&:hover {
.claim__menu-button {
display: block;
}
}
}
.claim-preview__wrapper--notice {
@ -247,17 +253,6 @@
.claim-preview-info {
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,
@ -408,6 +403,10 @@
&:hover {
cursor: pointer;
.claim__menu-button {
display: block;
}
}
@media (min-width: $breakpoint-large) {
@ -456,12 +455,23 @@
}
.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;
color: var(--color-text);
font-size: var(--font-small);
min-height: 2rem;
.claim__menu-button {
right: 0.2rem;
top: var(--spacing-s);
}
@media (min-width: $breakpoint-small) {
min-height: 2.5rem;
}
@ -571,3 +581,36 @@
.claim-preview__null-label {
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;
}
.menu__button {
&:hover {
.icon {
border-radius: var(--border-radius);
background-color: var(--color-card-background-highlighted);
}
}
}
.menu__title {
&[aria-expanded='true'] {
background-color: var(--color-primary-alt);
@ -54,16 +63,7 @@
animation: menu-animate-in var(--animation-duration) var(--animation-style);
border-bottom-left-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-radius: var(--border-radius);
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 {
display: flex;
align-items: center;

View file

@ -36,7 +36,7 @@
--color-button-primary-bg-hover: var(--color-primary-alt-2);
--color-button-primary-hover-text: var(--color-primary-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-bg-hover: #b9d0e9;
--color-button-alt-bg: var(--color-gray-1);

View file

@ -262,6 +262,12 @@ textarea {
background-color: var(--color-help-warning-bg);
color: var(--color-help-warning-text);
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 {

View file

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

View file

@ -1,8 +1,46 @@
// @flow
export function toHex(str: string): string {
var result = '';
for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16);
const array = Array.from(str);
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;
}
// 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;
}