Improve "moderator search" usability (#7043)
This commit is contained in:
commit
4ddf75f836
6 changed files with 291 additions and 160 deletions
9
ui/component/searchChannelField/index.js
Normal file
9
ui/component/searchChannelField/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
|
import SearchChannelField from './view';
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, perform)(SearchChannelField);
|
199
ui/component/searchChannelField/view.jsx
Normal file
199
ui/component/searchChannelField/view.jsx
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { isNameValid, parseURI } from 'lbry-redux';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import ClaimPreview from 'component/claimPreview';
|
||||||
|
import { FormField } from 'component/common/form-components/form-field';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import TagsSearch from 'component/tagsSearch';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import { getUriForSearchTerm } from 'util/search';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string,
|
||||||
|
labelAddNew: string,
|
||||||
|
labelFoundAction: string,
|
||||||
|
values: Array<string>, // [ 'name#id', 'name#id' ]
|
||||||
|
onAdd?: (channelUri: string) => void,
|
||||||
|
onRemove?: (channelUri: string) => void,
|
||||||
|
// --- perform ---
|
||||||
|
doToast: ({ message: string }) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SearchChannelField(props: Props) {
|
||||||
|
const { label, labelAddNew, labelFoundAction, values, onAdd, onRemove, doToast } = props;
|
||||||
|
|
||||||
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
|
const [searchTermError, setSearchTermError] = React.useState('');
|
||||||
|
const [searchUri, setSearchUri] = React.useState('');
|
||||||
|
const addTagRef = React.useRef<any>();
|
||||||
|
|
||||||
|
function parseUri(name: string) {
|
||||||
|
try {
|
||||||
|
return parseURI(name);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTag(newTags: Array<Tag>) {
|
||||||
|
// Ignoring multiple entries for now, although <TagsSearch> supports it.
|
||||||
|
const uri = parseUri(newTags[0].name);
|
||||||
|
|
||||||
|
if (uri && uri.isChannel && uri.claimName && uri.claimId) {
|
||||||
|
if (!values.includes(newTags[0].name)) {
|
||||||
|
if (onAdd) {
|
||||||
|
onAdd(newTags[0].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doToast({ message: __('Invalid channel URL "%url%"', { url: newTags[0].name }), isError: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTag(tagToRemove: Tag) {
|
||||||
|
const uri = parseUri(tagToRemove.name);
|
||||||
|
|
||||||
|
if (uri && uri.isChannel && uri.claimName && uri.claimId) {
|
||||||
|
if (values.includes(tagToRemove.name)) {
|
||||||
|
if (onRemove) {
|
||||||
|
onRemove(tagToRemove.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearchTerm() {
|
||||||
|
setSearchTerm('');
|
||||||
|
setSearchTermError('');
|
||||||
|
setSearchUri('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyPress(e) {
|
||||||
|
// We have to use 'key' instead of 'keyCode' in this event.
|
||||||
|
if (e.key === 'Enter' && addTagRef && addTagRef.current && addTagRef.current.click) {
|
||||||
|
e.preventDefault();
|
||||||
|
addTagRef.current.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFoundChannelRenderActionsFn() {
|
||||||
|
function handleFoundChannelClick(claim) {
|
||||||
|
if (claim && claim.name && claim.claim_id) {
|
||||||
|
addTag([{ name: claim.name + '#' + claim.claim_id }]);
|
||||||
|
clearSearchTerm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (claim) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={addTagRef}
|
||||||
|
requiresAuth
|
||||||
|
button="primary"
|
||||||
|
label={labelFoundAction}
|
||||||
|
onClick={() => handleFoundChannelClick(claim)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'searchTerm' sanitization
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!searchTerm) {
|
||||||
|
clearSearchTerm();
|
||||||
|
} else {
|
||||||
|
const isUrl = searchTerm.startsWith('https://') || searchTerm.startsWith('lbry://');
|
||||||
|
const autoAlias = !isUrl && !searchTerm.startsWith('@') ? '@' : '';
|
||||||
|
|
||||||
|
const [uri, error] = getUriForSearchTerm(`${autoAlias}${searchTerm}`);
|
||||||
|
setSearchTermError(error ? __('Something not quite right..') : '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { streamName, channelName, isChannel } = parseURI(uri);
|
||||||
|
|
||||||
|
if (!isChannel && streamName && isNameValid(streamName)) {
|
||||||
|
setSearchTermError(__('Not a channel (prefix with "@", or enter the channel URL)'));
|
||||||
|
setSearchUri('');
|
||||||
|
} else if (isChannel && channelName && isNameValid(channelName)) {
|
||||||
|
setSearchUri(uri);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setSearchTermError(e.message);
|
||||||
|
setSearchUri('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchTerm, setSearchTermError]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="search__channel tag--blocked-words">
|
||||||
|
<TagsSearch
|
||||||
|
label={label}
|
||||||
|
labelAddNew={labelAddNew}
|
||||||
|
tagsPassedIn={values.map((x) => ({ name: x }))}
|
||||||
|
onSelect={addTag}
|
||||||
|
onRemove={removeTag}
|
||||||
|
disableAutoFocus
|
||||||
|
hideInputField
|
||||||
|
hideSuggestions
|
||||||
|
disableControlTags
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="search__channel--popup">
|
||||||
|
<FormField
|
||||||
|
type="text"
|
||||||
|
name="moderator_search"
|
||||||
|
className="form-field--address"
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
{labelAddNew}
|
||||||
|
<Icon
|
||||||
|
customTooltipText={__(HELP.CHANNEL_SEARCH)}
|
||||||
|
className="icon--help"
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
tooltip
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
placeholder={__('Enter full channel name or URL')}
|
||||||
|
value={searchTerm}
|
||||||
|
error={searchTermError}
|
||||||
|
onKeyPress={(e) => handleKeyPress(e)}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{searchUri && (
|
||||||
|
<div className="search__channel--popup-results">
|
||||||
|
<ClaimPreview
|
||||||
|
uri={searchUri}
|
||||||
|
hideMenu
|
||||||
|
hideRepostLabel
|
||||||
|
disableNavigation
|
||||||
|
showNullPlaceholder
|
||||||
|
properties={''}
|
||||||
|
renderActions={getFoundChannelRenderActionsFn()}
|
||||||
|
empty={
|
||||||
|
<div className="claim-preview claim-preview--inactive claim-preview--large claim-preview__empty">
|
||||||
|
{__('Channel not found')}
|
||||||
|
<Icon
|
||||||
|
customTooltipText={__(HELP.CHANNEL_SEARCH)}
|
||||||
|
className="icon--help"
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
tooltip
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const HELP = {
|
||||||
|
CHANNEL_SEARCH: 'Enter the full channel name or URL to search.\n\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8',
|
||||||
|
};
|
|
@ -16,7 +16,6 @@ import {
|
||||||
selectFetchingBlockedWords,
|
selectFetchingBlockedWords,
|
||||||
selectModerationDelegatesById,
|
selectModerationDelegatesById,
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
|
@ -36,7 +35,6 @@ const perform = (dispatch) => ({
|
||||||
commentModRemoveDelegate: (modChanId, modChanName, creatorChannelClaim) =>
|
commentModRemoveDelegate: (modChanId, modChanName, creatorChannelClaim) =>
|
||||||
dispatch(doCommentModRemoveDelegate(modChanId, modChanName, creatorChannelClaim)),
|
dispatch(doCommentModRemoveDelegate(modChanId, modChanName, creatorChannelClaim)),
|
||||||
commentModListDelegates: (creatorChannelClaim) => dispatch(doCommentModListDelegates(creatorChannelClaim)),
|
commentModListDelegates: (creatorChannelClaim) => dispatch(doCommentModListDelegates(creatorChannelClaim)),
|
||||||
doToast: (options) => dispatch(doToast(options)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(SettingsCreatorPage);
|
export default connect(select, perform)(SettingsCreatorPage);
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import TagsSearch from 'component/tagsSearch';
|
import TagsSearch from 'component/tagsSearch';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Button from 'component/button';
|
|
||||||
import ChannelSelector from 'component/channelSelector';
|
import ChannelSelector from 'component/channelSelector';
|
||||||
|
import SearchChannelField from 'component/searchChannelField';
|
||||||
import SettingsRow from 'component/settingsRow';
|
import SettingsRow from 'component/settingsRow';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import { FormField } from 'component/common/form-components/form-field';
|
import { FormField } from 'component/common/form-components/form-field';
|
||||||
import Icon from 'component/common/icon';
|
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import { isNameValid, parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
import ClaimPreview from 'component/claimPreview';
|
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import { getUriForSearchTerm } from 'util/search';
|
|
||||||
|
|
||||||
const DEBOUNCE_REFRESH_MS = 1000;
|
const DEBOUNCE_REFRESH_MS = 1000;
|
||||||
|
|
||||||
|
@ -54,23 +50,19 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
commentModListDelegates,
|
commentModListDelegates,
|
||||||
fetchCreatorSettings,
|
fetchCreatorSettings,
|
||||||
updateCreatorSettings,
|
updateCreatorSettings,
|
||||||
doToast,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [commentsEnabled, setCommentsEnabled] = React.useState(true);
|
const [commentsEnabled, setCommentsEnabled] = React.useState(true);
|
||||||
const [mutedWordTags, setMutedWordTags] = React.useState([]);
|
const [mutedWordTags, setMutedWordTags] = React.useState([]);
|
||||||
const [moderatorTags, setModeratorTags] = React.useState([]);
|
const [moderatorUris, setModeratorUris] = React.useState([]);
|
||||||
const [moderatorSearchTerm, setModeratorSearchTerm] = React.useState('');
|
|
||||||
const [moderatorSearchError, setModeratorSearchError] = React.useState('');
|
|
||||||
const [moderatorSearchClaimUri, setModeratorSearchClaimUri] = React.useState('');
|
|
||||||
const [minTip, setMinTip] = React.useState(0);
|
const [minTip, setMinTip] = React.useState(0);
|
||||||
const [minSuper, setMinSuper] = React.useState(0);
|
const [minSuper, setMinSuper] = React.useState(0);
|
||||||
const [slowModeMin, setSlowModeMin] = React.useState(0);
|
const [slowModeMin, setSlowModeMin] = React.useState(0);
|
||||||
const [lastUpdated, setLastUpdated] = React.useState(1);
|
const [lastUpdated, setLastUpdated] = React.useState(1);
|
||||||
|
|
||||||
const pushSlowModeMinDebounced = React.useMemo(() => debounce(pushSlowModeMin, 1000), []);
|
const pushSlowModeMinDebounced = React.useMemo(() => debounce(pushSlowModeMin, 1000), []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
const pushMinTipDebounced = React.useMemo(() => debounce(pushMinTip, 1000), []);
|
const pushMinTipDebounced = React.useMemo(() => debounce(pushMinTip, 1000), []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
const pushMinSuperDebounced = React.useMemo(() => debounce(pushMinSuper, 1000), []);
|
const pushMinSuperDebounced = React.useMemo(() => debounce(pushMinSuper, 1000), []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
@ -79,8 +71,8 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
* Updates corresponding GUI states with the given PerChannelSettings values.
|
* Updates corresponding GUI states with the given PerChannelSettings values.
|
||||||
*
|
*
|
||||||
* @param settings
|
* @param settings
|
||||||
* @param fullSync If true, update all states and consider 'undefined' settings as "cleared/false";
|
* @param fullSync If true, update all states and consider 'undefined'
|
||||||
* if false, only update defined settings.
|
* settings as "cleared/false"; if false, only update defined settings.
|
||||||
*/
|
*/
|
||||||
function settingsToStates(settings: PerChannelSettings, fullSync: boolean) {
|
function settingsToStates(settings: PerChannelSettings, fullSync: boolean) {
|
||||||
const doSetMutedWordTags = (words: Array<string>) => {
|
const doSetMutedWordTags = (words: Array<string>) => {
|
||||||
|
@ -137,6 +129,31 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
updateCreatorSettings(activeChannelClaim, { min_tip_amount_super_chat: value });
|
updateCreatorSettings(activeChannelClaim, { min_tip_amount_super_chat: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseModUri(uri) {
|
||||||
|
try {
|
||||||
|
return parseURI(uri);
|
||||||
|
} catch (e) {}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModeratorAdded(channelUri: string) {
|
||||||
|
setModeratorUris([...moderatorUris, channelUri]);
|
||||||
|
const parsedUri = parseModUri(channelUri);
|
||||||
|
if (parsedUri && parsedUri.claimId && parsedUri.claimName) {
|
||||||
|
commentModAddDelegate(parsedUri.claimId, parsedUri.claimName, activeChannelClaim);
|
||||||
|
setLastUpdated(Date.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModeratorRemoved(channelUri: string) {
|
||||||
|
setModeratorUris(moderatorUris.filter((x) => x !== channelUri));
|
||||||
|
const parsedUri = parseModUri(channelUri);
|
||||||
|
if (parsedUri && parsedUri.claimId && parsedUri.claimName) {
|
||||||
|
commentModRemoveDelegate(parsedUri.claimId, parsedUri.claimName, activeChannelClaim);
|
||||||
|
setLastUpdated(Date.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addMutedWords(newTags: Array<Tag>) {
|
function addMutedWords(newTags: Array<Tag>) {
|
||||||
const validatedNewTags = [];
|
const validatedNewTags = [];
|
||||||
newTags.forEach((newTag) => {
|
newTags.forEach((newTag) => {
|
||||||
|
@ -162,74 +179,9 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
setLastUpdated(Date.now());
|
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(claim) {
|
|
||||||
if (claim && claim.name && claim.claim_id) {
|
|
||||||
addModerator([{ name: claim.name + '#' + claim.claim_id }]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
// 'moderatorSearchTerm' to 'moderatorSearchClaimUri'
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!moderatorSearchTerm) {
|
|
||||||
setModeratorSearchError('');
|
|
||||||
setModeratorSearchClaimUri('');
|
|
||||||
} else {
|
|
||||||
const [searchUri, error] = getUriForSearchTerm(moderatorSearchTerm);
|
|
||||||
setModeratorSearchError(error ? __('Something not quite right..') : '');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { streamName, channelName, isChannel } = parseURI(searchUri);
|
|
||||||
|
|
||||||
if (!isChannel && streamName && isNameValid(streamName)) {
|
|
||||||
setModeratorSearchError(__('Not a channel (prefix with "@", or enter the channel URL)'));
|
|
||||||
setModeratorSearchClaimUri('');
|
|
||||||
} else if (isChannel && channelName && isNameValid(channelName)) {
|
|
||||||
setModeratorSearchClaimUri(searchUri);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (moderatorSearchTerm !== '@') {
|
|
||||||
setModeratorSearchError('');
|
|
||||||
}
|
|
||||||
setModeratorSearchClaimUri('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [moderatorSearchTerm, setModeratorSearchError]);
|
|
||||||
|
|
||||||
// Update local moderator states with data from API.
|
// Update local moderator states with data from API.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
commentModListDelegates(activeChannelClaim);
|
commentModListDelegates(activeChannelClaim);
|
||||||
|
@ -239,15 +191,9 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
if (activeChannelClaim) {
|
if (activeChannelClaim) {
|
||||||
const delegates = moderationDelegatesById[activeChannelClaim.claim_id];
|
const delegates = moderationDelegatesById[activeChannelClaim.claim_id];
|
||||||
if (delegates) {
|
if (delegates) {
|
||||||
setModeratorTags(
|
setModeratorUris(delegates.map((d) => `${d.channelName}#${d.channelId}`));
|
||||||
delegates.map((d) => {
|
|
||||||
return {
|
|
||||||
name: d.channelName + '#' + d.channelId,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setModeratorTags([]);
|
setModeratorUris([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [activeChannelClaim, moderationDelegatesById]);
|
}, [activeChannelClaim, moderationDelegatesById]);
|
||||||
|
@ -400,6 +346,17 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow title={__('Moderators')} subtitle={__(HELP.MODERATORS)} multirow>
|
||||||
|
<SearchChannelField
|
||||||
|
label={__('Moderators')}
|
||||||
|
labelAddNew={__('Add moderator')}
|
||||||
|
labelFoundAction={__('Add')}
|
||||||
|
values={moderatorUris}
|
||||||
|
onAdd={handleModeratorAdded}
|
||||||
|
onRemove={handleModeratorRemoved}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow title={__('Filter')} subtitle={__(HELP.BLOCKED_WORDS)} multirow>
|
<SettingsRow title={__('Filter')} subtitle={__(HELP.BLOCKED_WORDS)} multirow>
|
||||||
<div className="tag--blocked-words">
|
<div className="tag--blocked-words">
|
||||||
<TagsSearch
|
<TagsSearch
|
||||||
|
@ -416,67 +373,6 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow title={__('Moderators')} subtitle={__(HELP.MODERATORS)} multirow>
|
|
||||||
<div className="tag--blocked-words">
|
|
||||||
<TagsSearch
|
|
||||||
label={__('Moderators')}
|
|
||||||
labelAddNew={__('Add moderator')}
|
|
||||||
onRemove={removeModerator}
|
|
||||||
onSelect={addModerator}
|
|
||||||
tagsPassedIn={moderatorTags}
|
|
||||||
disableAutoFocus
|
|
||||||
hideInputField
|
|
||||||
hideSuggestions
|
|
||||||
disableControlTags
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
type="text"
|
|
||||||
name="moderator_search"
|
|
||||||
className="form-field--address"
|
|
||||||
label={
|
|
||||||
<>
|
|
||||||
{__('Search channel')}
|
|
||||||
<Icon
|
|
||||||
customTooltipText={__(HELP.MODERATOR_SEARCH)}
|
|
||||||
className="icon--help"
|
|
||||||
icon={ICONS.HELP}
|
|
||||||
tooltip
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
placeholder={__('Enter a @username or URL')}
|
|
||||||
value={moderatorSearchTerm}
|
|
||||||
onChange={(e) => setModeratorSearchTerm(e.target.value)}
|
|
||||||
error={moderatorSearchError}
|
|
||||||
/>
|
|
||||||
{moderatorSearchClaimUri && (
|
|
||||||
<div className="section">
|
|
||||||
<ClaimPreview
|
|
||||||
key={moderatorSearchClaimUri}
|
|
||||||
uri={moderatorSearchClaimUri}
|
|
||||||
// type={'small'}
|
|
||||||
// showNullPlaceholder
|
|
||||||
hideMenu
|
|
||||||
hideRepostLabel
|
|
||||||
disableNavigation
|
|
||||||
properties={''}
|
|
||||||
renderActions={(claim) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
requiresAuth
|
|
||||||
button="primary"
|
|
||||||
label={__('Add as moderator')}
|
|
||||||
onClick={() => handleChannelSearchSelect(claim)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -137,3 +137,40 @@
|
||||||
max-width: 15rem;
|
max-width: 15rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search__channel {
|
||||||
|
margin-bottom: var(--spacing-l);
|
||||||
|
|
||||||
|
fieldset-section {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search__channel--popup {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search__channel--popup-results {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #21252980;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.claim-preview__title {
|
||||||
|
color: #eff1f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--uri-indicator,
|
||||||
|
.media__subtitle {
|
||||||
|
color: #d8dde1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon--help {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -230,10 +230,6 @@
|
||||||
.settings__row--value {
|
.settings__row--value {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
fieldset-section:not(:only-child) {
|
|
||||||
margin-top: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset-section.radio {
|
fieldset-section.radio {
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
@ -242,10 +238,6 @@
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags--remove {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags__input-wrapper {
|
.tags__input-wrapper {
|
||||||
.tag__input {
|
.tag__input {
|
||||||
height: unset;
|
height: unset;
|
||||||
|
|
Loading…
Reference in a new issue