New moderation tools: block & mute #5572

Merged
neb-b merged 9 commits from block into master 2021-03-03 19:50:17 +01:00
21 changed files with 108 additions and 360 deletions
Showing only changes of commit d3a1822f91 - Show all commits

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -129,7 +129,7 @@ function ClaimListDiscover(props: Props) {
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) || (urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
(defaultTags && getParamFromTags(defaultTags)); (defaultTags && getParamFromTags(defaultTags));
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness; const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
const mutedAndBlockedChannelIds = mutedUris.concat(blockedUris).map((uri) => uri.split('#')[1]); const mutedAndBlockedChannelIds = Array.from(new Set(mutedUris.concat(blockedUris).map((uri) => uri.split('#')[1])));
const langParam = urlParams.get(CS.LANGUAGE_KEY) || null; const langParam = urlParams.get(CS.LANGUAGE_KEY) || null;
const languageParams = searchInLanguage const languageParams = searchInLanguage

View file

@ -33,7 +33,7 @@ function ClaimMenuList(props: Props) {
? claim.permanent_url ? claim.permanent_url
: claim.signing_channel && claim.signing_channel.permanent_url); : claim.signing_channel && claim.signing_channel.permanent_url);
if (!channelUri) { if (!channelUri || claimIsMine) {
return null; return null;
} }

View file

@ -24,7 +24,6 @@ import FileDownloadLink from 'component/fileDownloadLink';
import AbandonedChannelPreview from 'component/abandonedChannelPreview'; import AbandonedChannelPreview from 'component/abandonedChannelPreview';
import PublishPending from 'component/publishPending'; import PublishPending from 'component/publishPending';
import ClaimMenuList from 'component/claimMenuList'; import ClaimMenuList from 'component/claimMenuList';
import ChannelStakedIndicator from 'component/channelStakedIndicator';
import ClaimPreviewLoading from './claim-preview-loading'; import ClaimPreviewLoading from './claim-preview-loading';
import ClaimPreviewHidden from './claim-preview-no-mature'; import ClaimPreviewHidden from './claim-preview-no-mature';
import ClaimPreviewNoContent from './claim-preview-no-content'; import ClaimPreviewNoContent from './claim-preview-no-content';
@ -329,8 +328,8 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<div className="claim-preview__primary-actions"> <div className="claim-preview__primary-actions">
{!isChannelUri && signingChannel && ( {!isChannelUri && signingChannel && (
<div className="claim-preview__channel-staked"> <div className="claim-preview__channel-staked">
<ChannelThumbnail uri={signingChannel.permanent_url} hideStakedIndicator /> <ChannelThumbnail uri={signingChannel.permanent_url} />
<ChannelStakedIndicator uri={signingChannel.permanent_url} inline /> {/* <ChannelStakedIndicator uri={signingChannel.permanent_url} inline /> */}
</div> </div>
)} )}

View file

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

View file

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

View file

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

View file

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

View file

@ -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

@ -31,7 +31,6 @@ import ModalCommentAcknowledgement from 'modal/modalCommentAcknowledgement';
import ModalWalletSend from 'modal/modalWalletSend'; import ModalWalletSend from 'modal/modalWalletSend';
import ModalWalletReceive from 'modal/modalWalletReceive'; import ModalWalletReceive from 'modal/modalWalletReceive';
import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome'; import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome';
import ModalCreateChannel from 'modal/modalChannelCreate';
import ModalSetReferrer from 'modal/modalSetReferrer'; import ModalSetReferrer from 'modal/modalSetReferrer';
import ModalSignOut from 'modal/modalSignOut'; import ModalSignOut from 'modal/modalSignOut';
import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate'; import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate';
@ -129,8 +128,6 @@ function ModalRouter(props: Props) {
return <ModalWalletReceive {...modalProps} />; return <ModalWalletReceive {...modalProps} />;
case MODALS.YOUTUBE_WELCOME: case MODALS.YOUTUBE_WELCOME:
return <ModalYoutubeWelcome />; return <ModalYoutubeWelcome />;
case MODALS.CREATE_CHANNEL:
return <ModalCreateChannel {...modalProps} />;
case MODALS.SET_REFERRER: case MODALS.SET_REFERRER:
return <ModalSetReferrer {...modalProps} />; return <ModalSetReferrer {...modalProps} />;
case MODALS.SIGN_OUT: case MODALS.SIGN_OUT:

View file

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

View file

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

View file

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

View file

@ -1,13 +1,14 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as REACTION_TYPES from 'constants/reactions'; import * as REACTION_TYPES from 'constants/reactions';
import { Lbry, buildURI, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux'; import { Lbry, parseURI, buildURI, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux';
import { doToast, doSeeNotifications } from 'redux/actions/notifications'; import { doToast, doSeeNotifications } from 'redux/actions/notifications';
import { import {
makeSelectCommentIdsForUri, makeSelectCommentIdsForUri,
makeSelectMyReactionsForComment, makeSelectMyReactionsForComment,
makeSelectOthersReactionsForComment, makeSelectOthersReactionsForComment,
selectPendingCommentReacts, selectPendingCommentReacts,
selectModerationBlockList,
} from 'redux/selectors/comments'; } from 'redux/selectors/comments';
import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications'; import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
@ -238,9 +239,15 @@ export function doCommentCreate(comment: string = '', claim_id: string = '', par
type: ACTIONS.COMMENT_CREATE_FAILED, type: ACTIONS.COMMENT_CREATE_FAILED,
data: error, data: error,
}); });
let toastMessage = __('Unable to create comment, please try again later.');
if (error && error.message === 'channel is blocked by publisher') {
toastMessage = __('Unable to comment. This channel has blocked you.');
}
dispatch( dispatch(
doToast({ doToast({
message: 'Unable to create comment, please try again later.', message: toastMessage,
isError: true, isError: true,
}) })
); );
@ -568,3 +575,42 @@ export function doFetchModBlockedList() {
}); });
}; };
} }
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

@ -372,9 +372,9 @@ $metadata-z-index: 1;
.channel-staked__wrapper { .channel-staked__wrapper {
display: flex; display: flex;
position: absolute; position: absolute;
padding: 0.25rem; padding: 0.2rem;
bottom: -0.75rem; bottom: -0.75rem;
left: -0.75rem; left: -0.8rem;
background-color: var(--color-card-background); background-color: var(--color-card-background);
border-radius: 50%; border-radius: 50%;
} }

View file

@ -1,7 +1,5 @@
// @flow // @flow
const EMOJI_REGEX = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*/g;
export function toHex(str: string): string { export function toHex(str: string): string {
const array = Array.from(str); const array = Array.from(str);
@ -9,14 +7,9 @@ export function toHex(str: string): string {
for (var i = 0; i < array.length; i++) { for (var i = 0; i < array.length; i++) {
const val = array[i]; const val = array[i];
const utf = toUTF8Array(val)
const isEmoji = EMOJI_REGEX.test(val); .map((num) => num.toString(16))
.join('');
const utf = isEmoji
? toUTF8Array(val)
.map((num) => num.toString(16))
.join('')
: val.charCodeAt(0).toString(16);
result += utf; result += utf;
} }
@ -24,7 +17,9 @@ export function toHex(str: string): string {
return result; return result;
} }
function toUTF8Array(str) { // https://gist.github.com/joni/3760795
// See comment that fixes an issue in the original gist
function toUTF8Array(str: string): Array<number> {
var utf8 = []; var utf8 = [];
for (var i = 0; i < str.length; i++) { for (var i = 0; i < str.length; i++) {
var charcode = str.charCodeAt(i); var charcode = str.charCodeAt(i);
@ -46,5 +41,6 @@ function toUTF8Array(str) {
); );
} }
} }
return utf8; return utf8;
} }