Moderator Delegation GUI
This commit is contained in:
parent
60780db94b
commit
337cfd8769
21 changed files with 700 additions and 114 deletions
|
@ -1302,7 +1302,7 @@
|
|||
"Blocked": "Blocked",
|
||||
"Unblock": "Unblock",
|
||||
"Unblocking...": "Unblocking...",
|
||||
"Channel blocked. You will not see them again.": "Channel blocked. You will not see them again.",
|
||||
"Channel \"%channel%\" blocked.": "Channel \"%channel%\" blocked.",
|
||||
"Mute Channel": "Mute Channel",
|
||||
"Unmute Channel": "Unmute Channel",
|
||||
"Muted": "Muted",
|
||||
|
@ -1690,13 +1690,32 @@
|
|||
"Transaction limit reached. Try reducing the Description length.": "Transaction limit reached. Try reducing the Description length.",
|
||||
"You do not have any muted channels": "You do not have any muted channels",
|
||||
"You do not have any blocked channels": "You do not have any blocked channels",
|
||||
"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.": "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.",
|
||||
"You do not have any globally-blocked channels": "You do not have any globally-blocked channels",
|
||||
"You do not have any blocked channels as a moderator": "You do not have any blocked channels as a moderator",
|
||||
"Blocked channels will be invisible to you in the app. They will not be able to comment on your content, nor reply to your comments left on other channels' content.": "Blocked channels will be invisible to you in the app. They will not be able to comment on your content, nor reply to your comments left on other channels' content.",
|
||||
"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.": "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.",
|
||||
"List of channels that you have blocked as a moderator. To unblock a channel, notify the content creator.": "List of channels that you have blocked as a moderator. To unblock a channel, notify the content creator.",
|
||||
"This is the global block list.": "This is the global block list.",
|
||||
"This channel is blocked": "This channel is blocked",
|
||||
"This channel is muted": "This channel is muted",
|
||||
"Are you sure you want to view this content? Viewing will not unblock @%channel%": "Are you sure you want to view this content? Viewing will not unblock @%channel%",
|
||||
"Are you sure you want to view this content? Viewing will not unmute @%channel%": "Are you sure you want to view this content? Viewing will not unmute @%channel%",
|
||||
"View Content": "View Content",
|
||||
"Global": "Global",
|
||||
"Moderator": "Moderator",
|
||||
"Global Unblock Channel": "Global Unblock Channel",
|
||||
"Global Block Channel": "Global Block Channel",
|
||||
"Moderator tools": "Moderator tools",
|
||||
"Global Block": "Global Block",
|
||||
"Block this channel as global admin": "Block this channel as global admin",
|
||||
"Moderator Block": "Moderator Block",
|
||||
"Block this channel on behalf of %creator%": "Block this channel on behalf of %creator%",
|
||||
"creator": "creator",
|
||||
"Invalid channel URL \"%url%\"": "Invalid channel URL \"%url%\"",
|
||||
"Delegation": "Delegation",
|
||||
"Add moderator": "Add moderator",
|
||||
"Add moderators": "Add moderators",
|
||||
"Add moderator channel URL (e.g. \"@lbry#3f\")": "Add moderator channel URL (e.g. \"@lbry#3f\")",
|
||||
"Mute (m)": "Mute (m)",
|
||||
"Playback Rate (<, >)": "Playback Rate (<, >)",
|
||||
"Fullscreen (f)": "Fullscreen (f)",
|
||||
|
|
|
@ -1,14 +1,57 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doCommentModUnBlock, doCommentModBlock } from 'redux/actions/comments';
|
||||
import { makeSelectChannelIsBlocked, makeSelectUriIsBlockingOrUnBlocking } from 'redux/selectors/comments';
|
||||
import { makeSelectClaimIdForUri } from 'lbry-redux';
|
||||
import {
|
||||
doCommentModUnBlock,
|
||||
doCommentModBlock,
|
||||
doCommentModBlockAsAdmin,
|
||||
doCommentModUnBlockAsAdmin,
|
||||
doCommentModUnBlockAsModerator,
|
||||
doCommentModBlockAsModerator,
|
||||
} from 'redux/actions/comments';
|
||||
import {
|
||||
makeSelectChannelIsBlocked,
|
||||
makeSelectChannelIsAdminBlocked,
|
||||
makeSelectChannelIsModeratorBlockedForCreator,
|
||||
makeSelectUriIsBlockingOrUnBlocking,
|
||||
makeSelectIsTogglingForDelegator,
|
||||
} from 'redux/selectors/comments';
|
||||
|
||||
import { BLOCK_LEVEL } from 'constants/comment';
|
||||
import ChannelBlockButton from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
isBlocked: makeSelectChannelIsBlocked(props.uri)(state),
|
||||
isBlockingOrUnBlocking: makeSelectUriIsBlockingOrUnBlocking(props.uri)(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
let isBlocked;
|
||||
let isToggling;
|
||||
|
||||
switch (props.blockLevel) {
|
||||
default:
|
||||
case BLOCK_LEVEL.SELF:
|
||||
isBlocked = makeSelectChannelIsBlocked(props.uri)(state);
|
||||
break;
|
||||
|
||||
case BLOCK_LEVEL.MODERATOR:
|
||||
isBlocked = makeSelectChannelIsModeratorBlockedForCreator(props.uri, props.creatorUri)(state);
|
||||
isToggling = makeSelectIsTogglingForDelegator(props.uri, props.creatorUri)(state);
|
||||
break;
|
||||
|
||||
case BLOCK_LEVEL.ADMIN:
|
||||
isBlocked = makeSelectChannelIsAdminBlocked(props.uri)(state);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
isBlocked,
|
||||
isToggling,
|
||||
isBlockingOrUnBlocking: makeSelectUriIsBlockingOrUnBlocking(props.uri)(state),
|
||||
creatorId: makeSelectClaimIdForUri(props.creatorUri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, {
|
||||
doCommentModUnBlock,
|
||||
doCommentModBlock,
|
||||
doCommentModUnBlockAsAdmin,
|
||||
doCommentModBlockAsAdmin,
|
||||
doCommentModUnBlockAsModerator,
|
||||
doCommentModBlockAsModerator,
|
||||
})(ChannelBlockButton);
|
||||
|
|
|
@ -1,41 +1,95 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { BLOCK_LEVEL } from 'constants/comment';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
blockLevel?: string,
|
||||
creatorUri?: string,
|
||||
isBlocked: boolean,
|
||||
isBlockingOrUnBlocking: boolean,
|
||||
isToggling: boolean,
|
||||
doCommentModUnBlock: (string, boolean) => void,
|
||||
doCommentModBlock: (string, boolean) => void,
|
||||
doCommentModUnBlockAsAdmin: (string, string) => void,
|
||||
doCommentModBlockAsAdmin: (string, string) => void,
|
||||
doCommentModUnBlockAsModerator: (string, string, string) => void,
|
||||
doCommentModBlockAsModerator: (string, string, string) => void,
|
||||
};
|
||||
|
||||
function ChannelBlockButton(props: Props) {
|
||||
const { uri, doCommentModUnBlock, doCommentModBlock, isBlocked, isBlockingOrUnBlocking } = props;
|
||||
const {
|
||||
uri,
|
||||
blockLevel,
|
||||
creatorUri,
|
||||
doCommentModUnBlock,
|
||||
doCommentModBlock,
|
||||
doCommentModUnBlockAsAdmin,
|
||||
doCommentModBlockAsAdmin,
|
||||
doCommentModUnBlockAsModerator,
|
||||
doCommentModBlockAsModerator,
|
||||
isBlocked,
|
||||
isBlockingOrUnBlocking,
|
||||
isToggling,
|
||||
} = props;
|
||||
|
||||
function handleClick() {
|
||||
if (isBlocked) {
|
||||
doCommentModUnBlock(uri, false);
|
||||
} else {
|
||||
doCommentModBlock(uri, false);
|
||||
switch (blockLevel) {
|
||||
default:
|
||||
case BLOCK_LEVEL.SELF:
|
||||
if (isBlocked) {
|
||||
doCommentModUnBlock(uri, false);
|
||||
} else {
|
||||
doCommentModBlock(uri, false);
|
||||
}
|
||||
break;
|
||||
|
||||
case BLOCK_LEVEL.MODERATOR:
|
||||
if (creatorUri) {
|
||||
const { channelClaimId } = parseURI(creatorUri);
|
||||
if (isBlocked) {
|
||||
doCommentModUnBlockAsModerator(uri, channelClaimId, '');
|
||||
} else {
|
||||
doCommentModBlockAsModerator(uri, channelClaimId, '');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case BLOCK_LEVEL.ADMIN:
|
||||
if (isBlocked) {
|
||||
doCommentModUnBlockAsAdmin(uri, '');
|
||||
} else {
|
||||
doCommentModBlockAsAdmin(uri, '');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
button={isBlocked ? 'alt' : 'secondary'}
|
||||
label={
|
||||
isBlocked
|
||||
function getButtonText(blockLevel) {
|
||||
switch (blockLevel) {
|
||||
default:
|
||||
case BLOCK_LEVEL.SELF:
|
||||
case BLOCK_LEVEL.ADMIN:
|
||||
return isBlocked
|
||||
? isBlockingOrUnBlocking
|
||||
? __('Unblocking...')
|
||||
: __('Unblock')
|
||||
: isBlockingOrUnBlocking
|
||||
? __('Blocking...')
|
||||
: __('Block')
|
||||
}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
? __('Blocking...')
|
||||
: __('Block');
|
||||
|
||||
case BLOCK_LEVEL.MODERATOR:
|
||||
if (isToggling) {
|
||||
return isBlocked ? __('Unblocking...') : __('Blocking...');
|
||||
} else {
|
||||
return isBlocked ? __('Unblock') : __('Block');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <Button button={isBlocked ? 'alt' : 'secondary'} label={getButtonText(blockLevel)} onClick={handleClick} />;
|
||||
}
|
||||
|
||||
export default ChannelBlockButton;
|
||||
|
|
|
@ -13,8 +13,17 @@ import {
|
|||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { doChannelMute, doChannelUnmute } from 'redux/actions/blocked';
|
||||
import { doSetActiveChannel, doSetIncognito, doOpenModal } from 'redux/actions/app';
|
||||
import { doCommentModBlock, doCommentModUnBlock } from 'redux/actions/comments';
|
||||
import { makeSelectChannelIsBlocked } from 'redux/selectors/comments';
|
||||
import {
|
||||
doCommentModBlock,
|
||||
doCommentModUnBlock,
|
||||
doCommentModBlockAsAdmin,
|
||||
doCommentModUnBlockAsAdmin,
|
||||
} from 'redux/actions/comments';
|
||||
import {
|
||||
selectHasAdminChannel,
|
||||
makeSelectChannelIsBlocked,
|
||||
makeSelectChannelIsAdminBlocked,
|
||||
} from 'redux/selectors/comments';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { makeSelectSigningIsMine } from 'redux/selectors/content';
|
||||
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
||||
|
@ -33,6 +42,8 @@ const select = (state, props) => {
|
|||
channelIsBlocked: makeSelectChannelIsBlocked(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.channelUri, true)(state),
|
||||
channelIsAdminBlocked: makeSelectChannelIsAdminBlocked(props.uri)(state),
|
||||
isAdmin: selectHasAdminChannel(state),
|
||||
claimInCollection: makeSelectCollectionForIdHasClaimUrl(props.collectionId, permanentUri)(state),
|
||||
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||
|
@ -57,6 +68,9 @@ const perform = (dispatch) => ({
|
|||
doChannelUnmute: (channelUri) => dispatch(doChannelUnmute(channelUri)),
|
||||
doCommentModBlock: (channelUri) => dispatch(doCommentModBlock(channelUri)),
|
||||
doCommentModUnBlock: (channelUri) => dispatch(doCommentModUnBlock(channelUri)),
|
||||
doCommentModBlockAsAdmin: (commenterUri, blockerId) => dispatch(doCommentModBlockAsAdmin(commenterUri, blockerId)),
|
||||
doCommentModUnBlockAsAdmin: (commenterUri, blockerId) =>
|
||||
dispatch(doCommentModUnBlockAsAdmin(commenterUri, blockerId)),
|
||||
doChannelSubscribe: (subscription) => dispatch(doChannelSubscribe(subscription)),
|
||||
doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)),
|
||||
doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)),
|
||||
|
|
|
@ -29,10 +29,14 @@ type Props = {
|
|||
inline?: boolean,
|
||||
channelIsMuted: boolean,
|
||||
channelIsBlocked: boolean,
|
||||
channelIsAdminBlocked: boolean,
|
||||
isAdmin: boolean,
|
||||
doChannelMute: (string) => void,
|
||||
doChannelUnmute: (string) => void,
|
||||
doCommentModBlock: (string) => void,
|
||||
doCommentModUnBlock: (string) => void,
|
||||
doCommentModBlockAsAdmin: (string, string) => void,
|
||||
doCommentModUnBlockAsAdmin: (string, string) => void,
|
||||
isRepost: boolean,
|
||||
doCollectionEdit: (string, any) => void,
|
||||
hasClaimInWatchLater: boolean,
|
||||
|
@ -62,9 +66,13 @@ function ClaimMenuList(props: Props) {
|
|||
doChannelUnmute,
|
||||
channelIsMuted,
|
||||
channelIsBlocked,
|
||||
channelIsAdminBlocked,
|
||||
isAdmin,
|
||||
doCommentModBlock,
|
||||
doCommentModUnBlock,
|
||||
isRepost,
|
||||
doCommentModBlockAsAdmin,
|
||||
doCommentModUnBlockAsAdmin,
|
||||
doCollectionEdit,
|
||||
hasClaimInWatchLater,
|
||||
collectionId,
|
||||
|
@ -166,6 +174,14 @@ function ClaimMenuList(props: Props) {
|
|||
openModal(MODALS.SEND_TIP, { uri, isSupport: true });
|
||||
}
|
||||
|
||||
function handleToggleAdminBlock() {
|
||||
if (channelIsAdminBlocked) {
|
||||
doCommentModUnBlockAsAdmin(channelUri, '');
|
||||
} else {
|
||||
doCommentModBlockAsAdmin(channelUri, '');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopyLink() {
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
}
|
||||
|
@ -292,6 +308,15 @@ function ClaimMenuList(props: Props) {
|
|||
</div>
|
||||
</MenuItem>
|
||||
|
||||
{isAdmin && (
|
||||
<MenuItem className="comment__menu-option" onSelect={handleToggleAdminBlock}>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.GLOBE} />
|
||||
{channelIsAdminBlocked ? __('Global Unblock Channel') : __('Global Block Channel')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem className="comment__menu-option" onSelect={handleToggleMute}>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.MUTE} />
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { doCommentAbandon, doCommentPin, doCommentList, doCommentModBlock } from 'redux/actions/comments';
|
||||
import {
|
||||
doCommentAbandon,
|
||||
doCommentPin,
|
||||
doCommentList,
|
||||
doCommentModBlock,
|
||||
doCommentModBlockAsAdmin,
|
||||
doCommentModBlockAsModerator,
|
||||
} from 'redux/actions/comments';
|
||||
import { doChannelMute } from 'redux/actions/blocked';
|
||||
// import { doSetActiveChannel } from 'redux/actions/app';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectPlayingUri } from 'redux/selectors/content';
|
||||
import { selectModerationDelegatorsById } from 'redux/selectors/comments';
|
||||
import CommentMenuList from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
|
@ -14,6 +22,7 @@ const select = (state, props) => ({
|
|||
contentChannelPermanentUrl: makeSelectChannelPermUrlForClaimUri(props.uri)(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
moderationDelegatorsById: selectModerationDelegatorsById(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
|
@ -23,7 +32,10 @@ const perform = (dispatch) => ({
|
|||
pinComment: (commentId, remove) => dispatch(doCommentPin(commentId, remove)),
|
||||
fetchComments: (uri) => dispatch(doCommentList(uri)),
|
||||
// setActiveChannel: channelId => dispatch(doSetActiveChannel(channelId)),
|
||||
commentModBlock: (commentAuthor) => dispatch(doCommentModBlock(commentAuthor)),
|
||||
commentModBlock: (commenterUri) => dispatch(doCommentModBlock(commenterUri)),
|
||||
commentModBlockAsAdmin: (commenterUri, blockerId) => dispatch(doCommentModBlockAsAdmin(commenterUri, blockerId)),
|
||||
commentModBlockAsModerator: (commenterUri, creatorId, blockerId) =>
|
||||
dispatch(doCommentModBlockAsModerator(commenterUri, creatorId, blockerId)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentMenuList);
|
||||
|
|
|
@ -7,6 +7,7 @@ import Icon from 'component/common/icon';
|
|||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: ?Claim,
|
||||
clearPlayingUri: () => void,
|
||||
authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
|
||||
commentId: string, // sha256 digest identifying the comment
|
||||
|
@ -22,14 +23,18 @@ type Props = {
|
|||
activeChannelClaim: ?ChannelClaim,
|
||||
isTopLevel: boolean,
|
||||
commentModBlock: (string) => void,
|
||||
commentModBlockAsAdmin: (string, string) => void,
|
||||
commentModBlockAsModerator: (string, string, string) => void,
|
||||
playingUri: ?PlayingUri,
|
||||
disableEdit?: boolean,
|
||||
disableRemove?: boolean,
|
||||
moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } },
|
||||
};
|
||||
|
||||
function CommentMenuList(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
claim,
|
||||
authorUri,
|
||||
commentIsMine,
|
||||
commentId,
|
||||
|
@ -44,11 +49,30 @@ function CommentMenuList(props: Props) {
|
|||
handleEditComment,
|
||||
fetchComments,
|
||||
commentModBlock,
|
||||
commentModBlockAsAdmin,
|
||||
commentModBlockAsModerator,
|
||||
playingUri,
|
||||
disableEdit,
|
||||
disableRemove,
|
||||
moderationDelegatorsById,
|
||||
} = props;
|
||||
|
||||
const contentChannelClaim = !claim
|
||||
? null
|
||||
: claim.value_type === 'channel'
|
||||
? claim
|
||||
: claim.signing_channel && claim.is_channel_signature_valid
|
||||
? claim.signing_channel
|
||||
: null;
|
||||
|
||||
const activeModeratorInfo = activeChannelClaim && moderationDelegatorsById[activeChannelClaim.claim_id];
|
||||
const activeChannelIsCreator = activeChannelClaim && activeChannelClaim.permanent_url === contentChannelPermanentUrl;
|
||||
const activeChannelIsAdmin = activeChannelClaim && activeModeratorInfo && activeModeratorInfo.global;
|
||||
const activeChannelIsModerator =
|
||||
activeChannelClaim &&
|
||||
contentChannelClaim &&
|
||||
activeModeratorInfo &&
|
||||
Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id);
|
||||
|
||||
function handlePinComment(commentId, remove) {
|
||||
pinComment(commentId, remove).then(() => fetchComments(uri));
|
||||
|
@ -69,6 +93,18 @@ function CommentMenuList(props: Props) {
|
|||
muteChannel(authorUri);
|
||||
}
|
||||
|
||||
function blockCommentAsModerator() {
|
||||
if (activeChannelClaim && contentChannelClaim) {
|
||||
commentModBlockAsModerator(authorUri, contentChannelClaim.claim_id, activeChannelClaim.claim_id);
|
||||
}
|
||||
}
|
||||
|
||||
function blockCommentAsAdmin() {
|
||||
if (activeChannelClaim) {
|
||||
commentModBlockAsAdmin(authorUri, activeChannelClaim.claim_id);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuList className="menu__list">
|
||||
{activeChannelIsCreator && <div className="comment__menu-title">{__('Creator tools')}</div>}
|
||||
|
@ -128,6 +164,34 @@ function CommentMenuList(props: Props) {
|
|||
</MenuItem>
|
||||
)}
|
||||
|
||||
{(activeChannelIsAdmin || activeChannelIsModerator) && (
|
||||
<div className="comment__menu-title">{__('Moderator tools')}</div>
|
||||
)}
|
||||
|
||||
{!commentIsMine && activeChannelIsAdmin && (
|
||||
<MenuItem className="comment__menu-option" onSelect={blockCommentAsAdmin}>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.GLOBE} />
|
||||
{__('Global Block')}
|
||||
</div>
|
||||
<span className="comment__menu-help">{__('Block this channel as global admin')}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{!commentIsMine && activeChannelIsModerator && (
|
||||
<MenuItem className="comment__menu-option" onSelect={blockCommentAsModerator}>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.BLOCK} />
|
||||
{__('Moderator Block')}
|
||||
</div>
|
||||
<span className="comment__menu-help">
|
||||
{__('Block this channel on behalf of %creator%', {
|
||||
creator: contentChannelClaim ? contentChannelClaim.name : __('creator'),
|
||||
})}
|
||||
</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{activeChannelClaim && (
|
||||
<div className="comment__menu-active">
|
||||
<ChannelThumbnail uri={activeChannelClaim.permanent_url} />
|
||||
|
|
|
@ -392,6 +392,13 @@ export const icons = {
|
|||
<line x1="12" y1="12" x2="12" y2="16" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.GLOBE]: buildIcon(
|
||||
<g>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="2" y1="12" x2="22" y2="12" />
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.UNLOCK]: buildIcon(
|
||||
<g>
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
||||
|
|
|
@ -15,6 +15,7 @@ type Props = {
|
|||
doAddTag: (string) => void,
|
||||
onSelect?: (Tag) => void,
|
||||
hideSuggestions?: boolean,
|
||||
hideInputField?: boolean,
|
||||
suggestMature?: boolean,
|
||||
disableAutoFocus?: boolean,
|
||||
onRemove: (Tag) => void,
|
||||
|
@ -49,6 +50,7 @@ export default function TagsSearch(props: Props) {
|
|||
onSelect,
|
||||
onRemove,
|
||||
hideSuggestions,
|
||||
hideInputField,
|
||||
suggestMature,
|
||||
disableAutoFocus,
|
||||
placeholder,
|
||||
|
@ -189,16 +191,18 @@ export default function TagsSearch(props: Props) {
|
|||
/>
|
||||
))}
|
||||
</ul>
|
||||
<FormField
|
||||
autoFocus={!disableAutoFocus}
|
||||
className="tag__input"
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || __('gaming, crypto')}
|
||||
type="text"
|
||||
value={newTag}
|
||||
disabled={disabled}
|
||||
label={labelAddNew || __('Add Tags')}
|
||||
/>
|
||||
{!hideInputField && (
|
||||
<FormField
|
||||
autoFocus={!disableAutoFocus}
|
||||
className="tag__input"
|
||||
onChange={onChange}
|
||||
placeholder={placeholder || __('gaming, crypto')}
|
||||
type="text"
|
||||
value={newTag}
|
||||
disabled={disabled}
|
||||
label={labelAddNew || __('Add Tags')}
|
||||
/>
|
||||
)}
|
||||
{!hideSuggestions && (
|
||||
<section>
|
||||
<label>{labelSuggestions || (newTag.length ? __('Matching') : __('Known Tags'))}</label>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { doOpenModal } from 'redux/actions/app';
|
|||
import Wunderbar from './view';
|
||||
|
||||
const perform = (dispatch, ownProps) => ({
|
||||
doOpenMobileSearch: () => dispatch(doOpenModal(MODALS.MOBILE_SEARCH)),
|
||||
doOpenMobileSearch: (props) => dispatch(doOpenModal(MODALS.MOBILE_SEARCH, props)),
|
||||
});
|
||||
|
||||
export default connect(null, perform)(Wunderbar);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useIsMobile } from 'effects/use-screensize';
|
|||
import WunderbarSuggestions from 'component/wunderbarSuggestions';
|
||||
|
||||
type Props = {
|
||||
doOpenMobileSearch: () => void,
|
||||
doOpenMobileSearch: (any) => void,
|
||||
channelsOnly?: boolean,
|
||||
noTopSuggestion?: boolean,
|
||||
noBottomLinks?: boolean,
|
||||
|
@ -18,7 +18,7 @@ export default function WunderBar(props: Props) {
|
|||
const isMobile = useIsMobile();
|
||||
|
||||
return isMobile ? (
|
||||
<Button icon={ICONS.SEARCH} className="wunderbar__mobile-search" onClick={() => doOpenMobileSearch()} />
|
||||
<Button icon={ICONS.SEARCH} className="wunderbar__mobile-search" onClick={() => doOpenMobileSearch({ ...props })} />
|
||||
) : (
|
||||
<WunderbarSuggestions
|
||||
channelsOnly={channelsOnly}
|
||||
|
|
|
@ -160,3 +160,4 @@ export const LIVESTREAM_SOLID = 'LivestreamSolid';
|
|||
export const LIVESTREAM_MONOCHROME = 'LivestreamMono';
|
||||
export const STACK = 'stack';
|
||||
export const TIME = 'time';
|
||||
export const GLOBE = 'globe';
|
||||
|
|
|
@ -5,14 +5,24 @@ import WunderbarSuggestions from 'component/wunderbarSuggestions';
|
|||
|
||||
type Props = {
|
||||
closeModal: () => void,
|
||||
channelsOnly?: boolean,
|
||||
noTopSuggestion?: boolean,
|
||||
noBottomLinks?: boolean,
|
||||
customSelectAction?: (string) => void,
|
||||
};
|
||||
|
||||
export default function ModalMobileSearch(props: Props) {
|
||||
const { closeModal } = props;
|
||||
const { closeModal, channelsOnly, noTopSuggestion, noBottomLinks, customSelectAction } = props;
|
||||
|
||||
return (
|
||||
<Modal onAborted={closeModal} isOpen type="card">
|
||||
<WunderbarSuggestions isMobile />
|
||||
<WunderbarSuggestions
|
||||
isMobile
|
||||
channelsOnly={channelsOnly}
|
||||
noTopSuggestion={noTopSuggestion}
|
||||
noBottomLinks={noBottomLinks}
|
||||
customSelectAction={customSelectAction}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,31 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchModBlockedList } from 'redux/actions/comments';
|
||||
import { doFetchModBlockedList, doFetchCommentModAmIList } from 'redux/actions/comments';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { selectModerationBlockList, selectFetchingModerationBlockList } from 'redux/selectors/comments';
|
||||
import {
|
||||
selectModerationBlockList,
|
||||
selectAdminBlockList,
|
||||
selectModeratorBlockList,
|
||||
selectModeratorBlockListDelegatorsMap,
|
||||
selectFetchingModerationBlockList,
|
||||
selectModerationDelegatorsById,
|
||||
} from 'redux/selectors/comments';
|
||||
import { selectMyChannelClaims } from 'lbry-redux';
|
||||
import ListBlocked from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
mutedUris: selectMutedChannels(state),
|
||||
blockedUris: selectModerationBlockList(state),
|
||||
personalBlockList: selectModerationBlockList(state),
|
||||
adminBlockList: selectAdminBlockList(state),
|
||||
moderatorBlockList: selectModeratorBlockList(state),
|
||||
moderatorBlockListDelegatorsMap: selectModeratorBlockListDelegatorsMap(state),
|
||||
delegatorsById: selectModerationDelegatorsById(state),
|
||||
myChannelClaims: selectMyChannelClaims(state),
|
||||
fetchingModerationBlockList: selectFetchingModerationBlockList(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
fetchModBlockedList: () => dispatch(doFetchModBlockedList()),
|
||||
fetchModAmIList: () => dispatch(doFetchCommentModAmIList()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ListBlocked);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import { BLOCK_LEVEL } from 'constants/comment';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import ClaimList from 'component/claimList';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import Page from 'component/page';
|
||||
import Spinner from 'component/spinner';
|
||||
import Button from 'component/button';
|
||||
|
@ -11,64 +13,220 @@ import ChannelBlockButton from 'component/channelBlockButton';
|
|||
import ChannelMuteButton from 'component/channelMuteButton';
|
||||
import Yrbl from 'component/yrbl';
|
||||
|
||||
type Props = {
|
||||
mutedUris: ?Array<string>,
|
||||
blockedUris: ?Array<string>,
|
||||
fetchingModerationBlockList: boolean,
|
||||
fetchModBlockedList: () => void,
|
||||
const VIEW = {
|
||||
BLOCKED: 'blocked',
|
||||
ADMIN: 'admin',
|
||||
MODERATOR: 'moderator',
|
||||
MUTED: 'muted',
|
||||
};
|
||||
|
||||
const VIEW_BLOCKED = 'blocked';
|
||||
const VIEW_MUTED = 'muted';
|
||||
type Props = {
|
||||
mutedUris: ?Array<string>,
|
||||
personalBlockList: ?Array<string>,
|
||||
adminBlockList: ?Array<string>,
|
||||
moderatorBlockList: ?Array<string>,
|
||||
moderatorBlockListDelegatorsMap: { [string]: Array<string> },
|
||||
fetchingModerationBlockList: boolean,
|
||||
fetchModBlockedList: () => void,
|
||||
fetchModAmIList: () => void,
|
||||
delegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } },
|
||||
myChannelClaims: ?Array<ChannelClaim>,
|
||||
};
|
||||
|
||||
function ListBlocked(props: Props) {
|
||||
const { mutedUris, blockedUris, fetchingModerationBlockList, fetchModBlockedList } = props;
|
||||
const [viewMode, setViewMode] = usePersistedState('blocked-muted:display', VIEW_BLOCKED);
|
||||
const {
|
||||
mutedUris,
|
||||
personalBlockList,
|
||||
adminBlockList,
|
||||
moderatorBlockList,
|
||||
moderatorBlockListDelegatorsMap,
|
||||
fetchingModerationBlockList,
|
||||
fetchModBlockedList,
|
||||
fetchModAmIList,
|
||||
delegatorsById,
|
||||
myChannelClaims,
|
||||
} = props;
|
||||
const [viewMode, setViewMode] = usePersistedState('blocked-muted:display', VIEW.BLOCKED);
|
||||
|
||||
// Keep a local list to allow for undoing actions in this component
|
||||
const [localBlockedList, setLocalBlockedList] = React.useState(undefined);
|
||||
const [localPersonalList, setLocalPersonalList] = React.useState(undefined);
|
||||
const [localAdminList, setLocalAdminList] = React.useState(undefined);
|
||||
const [localModeratorList, setLocalModeratorList] = React.useState(undefined);
|
||||
const [localModeratorListDelegatorsMap, setLocalModeratorListDelegatorsMap] = React.useState(undefined);
|
||||
const [localMutedList, setLocalMutedList] = React.useState(undefined);
|
||||
|
||||
const hasLocalMuteList = localMutedList && localMutedList.length > 0;
|
||||
const hasLocalBlockList = localBlockedList && localBlockedList.length > 0;
|
||||
const stringifiedMutedChannels = JSON.stringify(mutedUris);
|
||||
const hasLocalPersonalList = localPersonalList && localPersonalList.length > 0;
|
||||
|
||||
const stringifiedMutedList = JSON.stringify(mutedUris);
|
||||
const stringifiedPersonalList = JSON.stringify(personalBlockList);
|
||||
const stringifiedAdminList = JSON.stringify(adminBlockList);
|
||||
const stringifiedModeratorList = JSON.stringify(moderatorBlockList);
|
||||
const stringifiedModeratorListDelegatorsMap = JSON.stringify(moderatorBlockListDelegatorsMap);
|
||||
|
||||
const stringifiedLocalAdminList = JSON.stringify(localAdminList);
|
||||
const stringifiedLocalModeratorList = JSON.stringify(localModeratorList);
|
||||
const stringifiedLocalModeratorListDelegatorsMap = JSON.stringify(localModeratorListDelegatorsMap);
|
||||
|
||||
const justMuted = localMutedList && mutedUris && localMutedList.length < mutedUris.length;
|
||||
const justBlocked = localBlockedList && blockedUris && localBlockedList.length < blockedUris.length;
|
||||
const stringifiedBlockedChannels = JSON.stringify(blockedUris);
|
||||
const showUris = (viewMode === VIEW_MUTED && hasLocalMuteList) || (viewMode === VIEW_BLOCKED && hasLocalBlockList);
|
||||
const justPersonalBlocked =
|
||||
localPersonalList && personalBlockList && localPersonalList.length < personalBlockList.length;
|
||||
|
||||
const isAdmin =
|
||||
myChannelClaims && myChannelClaims.some((c) => delegatorsById[c.claim_id] && delegatorsById[c.claim_id].global);
|
||||
const isModerator =
|
||||
myChannelClaims &&
|
||||
myChannelClaims.some(
|
||||
(c) => delegatorsById[c.claim_id] && Object.keys(delegatorsById[c.claim_id].delegators).length > 0
|
||||
);
|
||||
|
||||
const listForView = getLocalList(viewMode);
|
||||
const showUris = listForView && listForView.length > 0;
|
||||
|
||||
function getLocalList(view) {
|
||||
switch (view) {
|
||||
case VIEW.BLOCKED:
|
||||
return localPersonalList;
|
||||
case VIEW.ADMIN:
|
||||
return localAdminList;
|
||||
case VIEW.MODERATOR:
|
||||
return localModeratorList;
|
||||
case VIEW.MUTED:
|
||||
return localMutedList;
|
||||
}
|
||||
}
|
||||
|
||||
function getButtons(view, uri) {
|
||||
switch (view) {
|
||||
case VIEW.BLOCKED:
|
||||
return (
|
||||
<>
|
||||
<ChannelBlockButton uri={uri} />
|
||||
<ChannelMuteButton uri={uri} />
|
||||
</>
|
||||
);
|
||||
|
||||
case VIEW.ADMIN:
|
||||
return <ChannelBlockButton uri={uri} blockLevel={BLOCK_LEVEL.ADMIN} />;
|
||||
|
||||
case VIEW.MODERATOR:
|
||||
const delegatorUrisForBlockedUri = localModeratorListDelegatorsMap && localModeratorListDelegatorsMap[uri];
|
||||
if (!delegatorUrisForBlockedUri) return null;
|
||||
return (
|
||||
<>
|
||||
{delegatorUrisForBlockedUri.map((delegatorUri) => {
|
||||
return (
|
||||
<div className="block-list--delegator" key={delegatorUri}>
|
||||
<ul className="section content__non-clickable">
|
||||
<ClaimPreview uri={delegatorUri} hideMenu hideActions type="small" />
|
||||
</ul>
|
||||
<ChannelBlockButton uri={uri} blockLevel={BLOCK_LEVEL.MODERATOR} creatorUri={delegatorUri} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
case VIEW.MUTED:
|
||||
return (
|
||||
<>
|
||||
<ChannelMuteButton uri={uri} />
|
||||
<ChannelBlockButton uri={uri} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getHelpText(view) {
|
||||
switch (view) {
|
||||
case VIEW.BLOCKED:
|
||||
return "Blocked channels will be invisible to you in the app. They will not be able to comment on your content, nor reply to your comments left on other channels' content.";
|
||||
case VIEW.ADMIN:
|
||||
return 'This is the global block list.';
|
||||
case VIEW.MODERATOR:
|
||||
return 'List of channels that you have blocked as a moderator, along with the list of delegators.';
|
||||
case VIEW.MUTED:
|
||||
return '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.';
|
||||
}
|
||||
}
|
||||
|
||||
function getEmptyListTitle(view) {
|
||||
switch (view) {
|
||||
case VIEW.BLOCKED:
|
||||
return 'You do not have any blocked channels';
|
||||
case VIEW.MUTED:
|
||||
return 'You do not have any muted channels';
|
||||
case VIEW.ADMIN:
|
||||
return 'You do not have any globally-blocked channels';
|
||||
case VIEW.MODERATOR:
|
||||
return 'You do not have any blocked channels as a moderator';
|
||||
}
|
||||
}
|
||||
|
||||
function getEmptyListSubtitle(view) {
|
||||
switch (view) {
|
||||
case VIEW.BLOCKED:
|
||||
case VIEW.MUTED:
|
||||
return getHelpText(view);
|
||||
|
||||
case VIEW.ADMIN:
|
||||
case VIEW.MODERATOR:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isSourceListLarger(source, local) {
|
||||
// Comparing the length of stringified is not perfect, but what are the
|
||||
// chances of having different lists with the exact same length?
|
||||
return source && (!local || local.length < source.length);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const jsonMutedChannels = stringifiedMutedChannels && JSON.parse(stringifiedMutedChannels);
|
||||
const jsonMutedChannels = stringifiedMutedList && JSON.parse(stringifiedMutedList);
|
||||
if (!hasLocalMuteList && jsonMutedChannels && jsonMutedChannels.length > 0) {
|
||||
setLocalMutedList(jsonMutedChannels);
|
||||
}
|
||||
}, [stringifiedMutedChannels, hasLocalMuteList]);
|
||||
}, [stringifiedMutedList, hasLocalMuteList]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const jsonBlockedChannels = stringifiedBlockedChannels && JSON.parse(stringifiedBlockedChannels);
|
||||
if (!hasLocalBlockList && jsonBlockedChannels && jsonBlockedChannels.length > 0) {
|
||||
setLocalBlockedList(jsonBlockedChannels);
|
||||
const jsonBlockedChannels = stringifiedPersonalList && JSON.parse(stringifiedPersonalList);
|
||||
if (!hasLocalPersonalList && jsonBlockedChannels && jsonBlockedChannels.length > 0) {
|
||||
setLocalPersonalList(jsonBlockedChannels);
|
||||
}
|
||||
}, [stringifiedBlockedChannels, hasLocalBlockList]);
|
||||
}, [stringifiedPersonalList, hasLocalPersonalList]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (justMuted && stringifiedMutedChannels) {
|
||||
setLocalMutedList(JSON.parse(stringifiedMutedChannels));
|
||||
if (stringifiedAdminList && isSourceListLarger(stringifiedAdminList, stringifiedLocalAdminList)) {
|
||||
setLocalAdminList(JSON.parse(stringifiedAdminList));
|
||||
}
|
||||
}, [stringifiedMutedChannels, justMuted, setLocalMutedList]);
|
||||
}, [stringifiedAdminList, stringifiedLocalAdminList]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (justBlocked && stringifiedBlockedChannels) {
|
||||
setLocalBlockedList(JSON.parse(stringifiedBlockedChannels));
|
||||
if (stringifiedModeratorList && isSourceListLarger(stringifiedModeratorList, stringifiedLocalModeratorList)) {
|
||||
setLocalModeratorList(JSON.parse(stringifiedModeratorList));
|
||||
}
|
||||
}, [stringifiedBlockedChannels, justBlocked, setLocalBlockedList]);
|
||||
}, [stringifiedModeratorList, stringifiedLocalModeratorList]);
|
||||
|
||||
const mutedHelpText = __(
|
||||
'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.'
|
||||
);
|
||||
const blockedHelpText = __(
|
||||
"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."
|
||||
);
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
stringifiedModeratorListDelegatorsMap &&
|
||||
isSourceListLarger(stringifiedModeratorListDelegatorsMap, stringifiedLocalModeratorListDelegatorsMap)
|
||||
) {
|
||||
setLocalModeratorListDelegatorsMap(JSON.parse(stringifiedModeratorListDelegatorsMap));
|
||||
}
|
||||
}, [stringifiedModeratorListDelegatorsMap, stringifiedLocalModeratorListDelegatorsMap]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (justMuted && stringifiedMutedList) {
|
||||
setLocalMutedList(JSON.parse(stringifiedMutedList));
|
||||
}
|
||||
}, [stringifiedMutedList, justMuted, setLocalMutedList]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (justPersonalBlocked && stringifiedPersonalList) {
|
||||
setLocalPersonalList(JSON.parse(stringifiedPersonalList));
|
||||
}
|
||||
}, [stringifiedPersonalList, justPersonalBlocked, setLocalPersonalList]);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
|
@ -87,60 +245,74 @@ function ListBlocked(props: Props) {
|
|||
button="alt"
|
||||
label={__('Blocked')}
|
||||
className={classnames(`button-toggle`, {
|
||||
'button-toggle--active': viewMode === VIEW_BLOCKED,
|
||||
'button-toggle--active': viewMode === VIEW.BLOCKED,
|
||||
})}
|
||||
onClick={() => setViewMode(VIEW_BLOCKED)}
|
||||
onClick={() => setViewMode(VIEW.BLOCKED)}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
icon={ICONS.BLOCK}
|
||||
button="alt"
|
||||
label={__('Global')}
|
||||
className={classnames(`button-toggle`, {
|
||||
'button-toggle--active': viewMode === VIEW.ADMIN,
|
||||
})}
|
||||
onClick={() => setViewMode(VIEW.ADMIN)}
|
||||
/>
|
||||
)}
|
||||
{isModerator && (
|
||||
<Button
|
||||
icon={ICONS.BLOCK}
|
||||
button="alt"
|
||||
label={__('Moderator')}
|
||||
className={classnames(`button-toggle`, {
|
||||
'button-toggle--active': viewMode === VIEW.MODERATOR,
|
||||
})}
|
||||
onClick={() => setViewMode(VIEW.MODERATOR)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon={ICONS.MUTE}
|
||||
button="alt"
|
||||
label={__('Muted')}
|
||||
className={classnames(`button-toggle`, {
|
||||
'button-toggle--active': viewMode === VIEW_MUTED,
|
||||
'button-toggle--active': viewMode === VIEW.MUTED,
|
||||
})}
|
||||
onClick={() => setViewMode(VIEW_MUTED)}
|
||||
onClick={() => setViewMode(VIEW.MUTED)}
|
||||
/>
|
||||
</div>
|
||||
<div className="section__actions--inline">
|
||||
<Button icon={ICONS.REFRESH} button="alt" label={__('Refresh')} onClick={() => fetchModBlockedList()} />
|
||||
<Button
|
||||
icon={ICONS.REFRESH}
|
||||
button="alt"
|
||||
label={__('Refresh')}
|
||||
onClick={() => {
|
||||
fetchModBlockedList();
|
||||
fetchModAmIList();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showUris && <div className="help--notice">{viewMode === VIEW_MUTED ? mutedHelpText : blockedHelpText}</div>}
|
||||
{showUris && <div className="help--notice">{getHelpText(viewMode)}</div>}
|
||||
|
||||
{showUris ? (
|
||||
<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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className={viewMode === VIEW.MODERATOR ? 'block-list--moderator' : 'block-list'}>
|
||||
<ClaimList
|
||||
uris={getLocalList(viewMode)}
|
||||
showUnresolvedClaims
|
||||
showHiddenByUser
|
||||
hideMenu
|
||||
renderActions={(claim) => {
|
||||
return <div className="section__actions">{getButtons(viewMode, claim.permanent_url)}</div>;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="main--empty">
|
||||
<Yrbl
|
||||
title={
|
||||
viewMode === VIEW_MUTED
|
||||
? __('You do not have any muted channels')
|
||||
: __('You do not have any blocked channels')
|
||||
}
|
||||
subtitle={viewMode === VIEW_MUTED ? mutedHelpText : blockedHelpText}
|
||||
title={getEmptyListTitle(viewMode)}
|
||||
subtitle={getEmptyListSubtitle(viewMode)}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button button="primary" label={__('Go Home')} navigate="/" />
|
||||
|
|
|
@ -3,6 +3,9 @@ import SettingsCreatorPage from './view';
|
|||
import {
|
||||
doCommentBlockWords,
|
||||
doCommentUnblockWords,
|
||||
doCommentModAddDelegate,
|
||||
doCommentModRemoveDelegate,
|
||||
doCommentModListDelegates,
|
||||
doFetchCreatorSettings,
|
||||
doUpdateCreatorSettings,
|
||||
} from 'redux/actions/comments';
|
||||
|
@ -11,13 +14,16 @@ import {
|
|||
selectSettingsByChannelId,
|
||||
selectFetchingCreatorSettings,
|
||||
selectFetchingBlockedWords,
|
||||
selectModerationDelegatesById,
|
||||
} from 'redux/selectors/comments';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
|
||||
const select = (state) => ({
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
settingsByChannelId: selectSettingsByChannelId(state),
|
||||
fetchingCreatorSettings: selectFetchingCreatorSettings(state),
|
||||
fetchingBlockedWords: selectFetchingBlockedWords(state),
|
||||
moderationDelegatesById: selectModerationDelegatesById(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
|
@ -25,6 +31,12 @@ const perform = (dispatch) => ({
|
|||
commentUnblockWords: (channelClaim, words) => dispatch(doCommentUnblockWords(channelClaim, words)),
|
||||
fetchCreatorSettings: (channelClaimIds) => dispatch(doFetchCreatorSettings(channelClaimIds)),
|
||||
updateCreatorSettings: (channelClaim, settings) => dispatch(doUpdateCreatorSettings(channelClaim, settings)),
|
||||
commentModAddDelegate: (modChanId, modChanName, creatorChannelClaim) =>
|
||||
dispatch(doCommentModAddDelegate(modChanId, modChanName, creatorChannelClaim)),
|
||||
commentModRemoveDelegate: (modChanId, modChanName, creatorChannelClaim) =>
|
||||
dispatch(doCommentModRemoveDelegate(modChanId, modChanName, creatorChannelClaim)),
|
||||
commentModListDelegates: (creatorChannelClaim) => dispatch(doCommentModListDelegates(creatorChannelClaim)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SettingsCreatorPage);
|
||||
|
|
|
@ -8,6 +8,8 @@ import Spinner from 'component/spinner';
|
|||
import { FormField } from 'component/common/form-components/form-field';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import WunderBar from 'component/wunderbar';
|
||||
|
||||
const DEBOUNCE_REFRESH_MS = 1000;
|
||||
|
||||
|
@ -18,24 +20,35 @@ type Props = {
|
|||
settingsByChannelId: { [string]: PerChannelSettings },
|
||||
fetchingCreatorSettings: boolean,
|
||||
fetchingBlockedWords: boolean,
|
||||
moderationDelegatesById: { [string]: Array<{ channelId: string, channelName: string }> },
|
||||
commentBlockWords: (ChannelClaim, Array<string>) => void,
|
||||
commentUnblockWords: (ChannelClaim, Array<string>) => void,
|
||||
commentModAddDelegate: (string, string, ChannelClaim) => void,
|
||||
commentModRemoveDelegate: (string, string, ChannelClaim) => void,
|
||||
commentModListDelegates: (ChannelClaim) => void,
|
||||
fetchCreatorSettings: (Array<string>) => void,
|
||||
updateCreatorSettings: (ChannelClaim, PerChannelSettings) => void,
|
||||
doToast: ({ message: string }) => void,
|
||||
};
|
||||
|
||||
export default function SettingsCreatorPage(props: Props) {
|
||||
const {
|
||||
activeChannelClaim,
|
||||
settingsByChannelId,
|
||||
moderationDelegatesById,
|
||||
commentBlockWords,
|
||||
commentUnblockWords,
|
||||
commentModAddDelegate,
|
||||
commentModRemoveDelegate,
|
||||
commentModListDelegates,
|
||||
fetchCreatorSettings,
|
||||
updateCreatorSettings,
|
||||
doToast,
|
||||
} = props;
|
||||
|
||||
const [commentsEnabled, setCommentsEnabled] = React.useState(true);
|
||||
const [mutedWordTags, setMutedWordTags] = React.useState([]);
|
||||
const [moderatorTags, setModeratorTags] = React.useState([]);
|
||||
const [minTipAmountComment, setMinTipAmountComment] = React.useState(0);
|
||||
const [minTipAmountSuperChat, setMinTipAmountSuperChat] = React.useState(0);
|
||||
const [slowModeMinGap, setSlowModeMinGap] = React.useState(0);
|
||||
|
@ -97,6 +110,71 @@ export default function SettingsCreatorPage(props: Props) {
|
|||
setLastUpdated(Date.now());
|
||||
}
|
||||
|
||||
function addModerator(newTags: Array<Tag>) {
|
||||
// Ignoring multiple entries for now, although <TagsSearch> supports it.
|
||||
let modUri;
|
||||
try {
|
||||
modUri = parseURI(newTags[0].name);
|
||||
} catch (e) {}
|
||||
|
||||
if (modUri && modUri.isChannel && modUri.claimName && modUri.claimId) {
|
||||
if (!moderatorTags.some((modTag) => modTag.name === newTags[0].name)) {
|
||||
setModeratorTags([...moderatorTags, newTags[0]]);
|
||||
commentModAddDelegate(modUri.claimId, modUri.claimName, activeChannelClaim);
|
||||
setLastUpdated(Date.now());
|
||||
}
|
||||
} else {
|
||||
doToast({ message: __('Invalid channel URL "%url%"', { url: newTags[0].name }), isError: true });
|
||||
}
|
||||
}
|
||||
|
||||
function removeModerator(tagToRemove: Tag) {
|
||||
let modUri;
|
||||
try {
|
||||
modUri = parseURI(tagToRemove.name);
|
||||
} catch (e) {}
|
||||
|
||||
if (modUri && modUri.isChannel && modUri.claimName && modUri.claimId) {
|
||||
const newModeratorTags = moderatorTags.slice().filter((t) => t.name !== tagToRemove.name);
|
||||
setModeratorTags(newModeratorTags);
|
||||
commentModRemoveDelegate(modUri.claimId, modUri.claimName, activeChannelClaim);
|
||||
setLastUpdated(Date.now());
|
||||
}
|
||||
}
|
||||
|
||||
function handleChannelSearchSelect(value: string) {
|
||||
let uriInfo;
|
||||
try {
|
||||
uriInfo = parseURI(value);
|
||||
} catch (e) {}
|
||||
|
||||
if (uriInfo && uriInfo.path) {
|
||||
addModerator([{ name: uriInfo.path }]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update local moderator states with data from API.
|
||||
React.useEffect(() => {
|
||||
commentModListDelegates(activeChannelClaim);
|
||||
}, [activeChannelClaim, commentModListDelegates]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeChannelClaim) {
|
||||
const delegates = moderationDelegatesById[activeChannelClaim.claim_id];
|
||||
if (delegates) {
|
||||
setModeratorTags(
|
||||
delegates.map((d) => {
|
||||
return {
|
||||
name: d.channelName + '#' + d.channelId,
|
||||
};
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setModeratorTags([]);
|
||||
}
|
||||
}
|
||||
}, [activeChannelClaim, moderationDelegatesById]);
|
||||
|
||||
// Update local states with data from API.
|
||||
React.useEffect(() => {
|
||||
if (lastUpdated !== 0 && Date.now() - lastUpdated < DEBOUNCE_REFRESH_MS) {
|
||||
|
@ -235,6 +313,34 @@ export default function SettingsCreatorPage(props: Props) {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
<Card
|
||||
title={__('Delegation')}
|
||||
className="card--enable-overflow"
|
||||
actions={
|
||||
<div className="tag--blocked-words">
|
||||
<label>{__('Add moderator')}</label>
|
||||
<WunderBar
|
||||
channelsOnly
|
||||
noTopSuggestion
|
||||
noBottomLinks
|
||||
customSelectAction={handleChannelSearchSelect}
|
||||
placeholder={__('Add moderator')}
|
||||
/>
|
||||
<TagsSearch
|
||||
label={__('Moderators')}
|
||||
labelAddNew={__('Add moderators')}
|
||||
placeholder={__('Add moderator channel URL (e.g. "@lbry#3f")')}
|
||||
onRemove={removeModerator}
|
||||
onSelect={addModerator}
|
||||
tagsPassedIn={moderatorTags}
|
||||
disableAutoFocus
|
||||
hideInputField
|
||||
hideSuggestions
|
||||
disableControlTags
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
@import 'component/ads';
|
||||
@import 'component/animation';
|
||||
@import 'component/badge';
|
||||
@import 'component/block-list';
|
||||
@import 'component/button';
|
||||
@import 'component/card';
|
||||
@import 'component/channel';
|
||||
|
|
18
ui/scss/component/_block-list.scss
Normal file
18
ui/scss/component/_block-list.scss
Normal file
|
@ -0,0 +1,18 @@
|
|||
.block-list--moderator {
|
||||
.button__content {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.section__actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.block-list--delegator {
|
||||
padding: var(--spacing-s);
|
||||
|
||||
.button--alt,
|
||||
.button--secondary {
|
||||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
}
|
|
@ -318,6 +318,12 @@ $thumbnailWidthSmall: 1rem;
|
|||
font-size: var(--font-small);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: var(--spacing-xs);
|
||||
padding-top: var(--spacing-xs);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
|
|
@ -84,4 +84,8 @@
|
|||
background-color: var(--color-tag-words-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.tag__input {
|
||||
max-width: 40rem;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue