lbry-desktop/ui/page/settingsCreator/view.jsx
infinite-persistence 6de16813cb
i18n update
2021-07-26 23:31:03 +08:00

399 lines
14 KiB
JavaScript

// @flow
import * as React from 'react';
import Card from 'component/common/card';
import TagsSearch from 'component/tagsSearch';
import Page from 'component/page';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
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 { isNameValid, parseURI } from 'lbry-redux';
import ClaimPreview from 'component/claimPreview';
import { getUriForSearchTerm } from 'util/search';
const DEBOUNCE_REFRESH_MS = 1000;
const FEATURE_IS_READY = false;
type Props = {
activeChannelClaim: ChannelClaim,
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 [moderatorSearchTerm, setModeratorSearchTerm] = React.useState('');
const [moderatorSearchError, setModeratorSearchError] = React.useState('');
const [moderatorSearchClaimUri, setModeratorSearchClaimUri] = React.useState('');
const [minTipAmountComment, setMinTipAmountComment] = React.useState(0);
const [minTipAmountSuperChat, setMinTipAmountSuperChat] = React.useState(0);
const [slowModeMinGap, setSlowModeMinGap] = React.useState(0);
const [lastUpdated, setLastUpdated] = React.useState(1);
function settingsToStates(settings: PerChannelSettings) {
if (settings.comments_enabled !== undefined) {
setCommentsEnabled(settings.comments_enabled);
}
if (settings.min_tip_amount_comment !== undefined) {
setMinTipAmountComment(settings.min_tip_amount_comment);
}
if (settings.min_tip_amount_super_chat !== undefined) {
setMinTipAmountSuperChat(settings.min_tip_amount_super_chat);
}
if (settings.slow_mode_min_gap !== undefined) {
setSlowModeMinGap(settings.slow_mode_min_gap);
}
if (settings.words) {
const tagArray = Array.from(new Set(settings.words));
setMutedWordTags(
tagArray
.filter((t) => t !== '')
.map((x) => {
return { name: x };
})
);
}
}
function setSettings(newSettings: PerChannelSettings) {
settingsToStates(newSettings);
updateCreatorSettings(activeChannelClaim, newSettings);
setLastUpdated(Date.now());
}
function addMutedWords(newTags: Array<Tag>) {
const validatedNewTags = [];
newTags.forEach((newTag) => {
if (!mutedWordTags.some((tag) => tag.name === newTag.name)) {
validatedNewTags.push(newTag);
}
});
if (validatedNewTags.length !== 0) {
setMutedWordTags([...mutedWordTags, ...validatedNewTags]);
commentBlockWords(
activeChannelClaim,
validatedNewTags.map((x) => x.name)
);
setLastUpdated(Date.now());
}
}
function removeMutedWord(tagToRemove: Tag) {
const newMutedWordTags = mutedWordTags.slice().filter((t) => t.name !== tagToRemove.name);
setMutedWordTags(newMutedWordTags);
commentUnblockWords(activeChannelClaim, ['', tagToRemove.name]);
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.
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) {
// Still debouncing. Skip update.
return;
}
if (activeChannelClaim && settingsByChannelId && settingsByChannelId[activeChannelClaim.claim_id]) {
const channelSettings = settingsByChannelId[activeChannelClaim.claim_id];
settingsToStates(channelSettings);
}
}, [activeChannelClaim, settingsByChannelId, lastUpdated]);
// Re-sync list, mainly to correct any invalid settings.
React.useEffect(() => {
if (lastUpdated && activeChannelClaim) {
const timer = setTimeout(() => {
fetchCreatorSettings([activeChannelClaim.claim_id]);
}, DEBOUNCE_REFRESH_MS);
return () => clearTimeout(timer);
}
}, [lastUpdated, activeChannelClaim, fetchCreatorSettings]);
const isBusy =
!activeChannelClaim || !settingsByChannelId || settingsByChannelId[activeChannelClaim.claim_id] === undefined;
const isDisabled =
activeChannelClaim && settingsByChannelId && settingsByChannelId[activeChannelClaim.claim_id] === null;
return (
<Page
noFooter
noSideNavigation
backout={{
title: __('Creator settings'),
backLabel: __('Done'),
}}
className="card-stack"
>
<ChannelSelector hideAnon />
{isBusy && (
<div className="main--empty">
<Spinner />
</div>
)}
{isDisabled && (
<Card
title={__('Settings unavailable for this channel')}
subtitle={__("This channel isn't staking enough LBRY Credits to enable Creator Settings.")}
/>
)}
{!isBusy && !isDisabled && (
<>
<Card
title={__('General')}
actions={
<>
<FormField
type="checkbox"
name="comments_enabled"
label={__('Enable comments for channel.')}
checked={commentsEnabled}
onChange={() => setSettings({ comments_enabled: !commentsEnabled })}
/>
<FormField
name="slow_mode_min_gap"
label={__('Minimum time gap in seconds between comments (affects livestream chat as well).')}
min={0}
step={1}
type="number"
placeholder="1"
value={slowModeMinGap}
onChange={(e) => setSettings({ slow_mode_min_gap: parseInt(e.target.value) })}
/>
</>
}
/>
<Card
title={__('Filter')}
actions={
<div className="tag--blocked-words">
<TagsSearch
label={__('Muted words')}
labelAddNew={__('Add words')}
labelSuggestions={__('Suggestions')}
onRemove={removeMutedWord}
onSelect={addMutedWords}
disableAutoFocus
tagsPassedIn={mutedWordTags}
placeholder={__('Add words to block')}
hideSuggestions
disableControlTags
/>
</div>
}
/>
{FEATURE_IS_READY && (
<Card
title={__('Tip')}
actions={
<>
<FormField
name="min_tip_amount_comment"
label={
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
}
helper={__(
'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.'
)}
className="form-field--price-amount"
min={0}
step="any"
type="number"
placeholder="1"
value={minTipAmountComment}
onChange={(e) => setSettings({ min_tip_amount_comment: parseFloat(e.target.value) })}
/>
<FormField
name="min_tip_amount_super_chat"
label={
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
}
helper={__(
'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.'
)}
className="form-field--price-amount"
min={0}
step="any"
type="number"
placeholder="1"
value={minTipAmountSuperChat}
onChange={(e) => setSettings({ min_tip_amount_super_chat: parseFloat(e.target.value) })}
/>
</>
}
/>
)}
<Card
title={__('Delegation')}
className="card--enable-overflow"
actions={
<div className="tag--blocked-words">
<FormField
type="text"
name="moderator_search"
className="form-field--address"
label={__('Add moderator')}
placeholder={__('Enter a @username or URL')}
helper={__('examples: @channel, @channel#3, https://odysee.com/@Odysee:8, lbry://@Odysee#8')}
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>
)}
<TagsSearch
label={__('Moderators')}
labelAddNew={__('Add moderator')}
onRemove={removeModerator}
onSelect={addModerator}
tagsPassedIn={moderatorTags}
disableAutoFocus
hideInputField
hideSuggestions
disableControlTags
/>
</div>
}
/>
</>
)}
</Page>
);
}