Add/implement SettingsCreatorPage

This commit is contained in:
infinite-persistence 2021-04-21 17:25:06 +08:00 committed by Thomas Zarebczan
parent 54b17ee739
commit 2b70ad667b
10 changed files with 314 additions and 1 deletions

View file

@ -1860,6 +1860,17 @@
"Law URL": "Law URL", "Law URL": "Law URL",
"Clarification": "Clarification", "Clarification": "Clarification",
"Client name": "Client name", "Client name": "Client name",
"Creator settings": "Creator settings",
"Muted words": "Muted words",
"Add words": "Add words",
"Suggestions": "Suggestions",
"Add words to block": "Add words to block",
"Enable comments for channel.": "Enable comments for channel.",
"Minimum time gap in seconds for Slow Mode in livestream chat.": "Minimum time gap in seconds for Slow Mode in livestream chat.",
"Minimum %lbc% tip amount for comments": "Minimum %lbc% tip amount for comments",
"Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.": "Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.",
"Minimum %lbc% tip amount for hyperchats": "Minimum %lbc% tip amount for hyperchats",
"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.": "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.",
"We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.": "We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.", "We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.": "We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.",
"Help LBRY Save Crypto": "Help LBRY Save Crypto", "Help LBRY Save Crypto": "Help LBRY Save Crypto",
"The US government is attempting to destroy the cryptocurrency industry. Can you help?": "The US government is attempting to destroy the cryptocurrency industry. Can you help?", "The US government is attempting to destroy the cryptocurrency industry. Can you help?": "The US government is attempting to destroy the cryptocurrency industry. Can you help?",

View file

@ -5,6 +5,7 @@ import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
import SettingsPage from 'page/settings'; import SettingsPage from 'page/settings';
import SettingsNotificationsPage from 'page/settingsNotifications'; import SettingsNotificationsPage from 'page/settingsNotifications';
import SettingsAdvancedPage from 'page/settingsAdvanced'; import SettingsAdvancedPage from 'page/settingsAdvanced';
import SettingsCreatorPage from 'page/settingsCreator';
import HelpPage from 'page/help'; import HelpPage from 'page/help';
// @if TARGET='app' // @if TARGET='app'
import BackupPage from 'page/backup'; import BackupPage from 'page/backup';
@ -283,6 +284,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} /> <PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} /> <PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM}`} component={LiveStreamSetupPage} /> <PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM}`} component={LiveStreamSetupPage} />

View file

@ -38,6 +38,7 @@ exports.SETTINGS = 'settings';
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications'; exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
exports.SETTINGS_ADVANCED = 'settings/advanced'; exports.SETTINGS_ADVANCED = 'settings/advanced';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute'; exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
exports.SETTINGS_CREATOR = 'settings/creator';
exports.SHOW = 'show'; exports.SHOW = 'show';
exports.ACCOUNT = 'account'; exports.ACCOUNT = 'account';
exports.SEARCH = 'search'; exports.SEARCH = 'search';

View file

@ -16,7 +16,7 @@ import {
selectLanguage, selectLanguage,
selectShowMatureContent, selectShowMatureContent,
} from 'redux/selectors/settings'; } from 'redux/selectors/settings';
import { doWalletStatus, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux'; import { doWalletStatus, selectMyChannelUrls, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
import SettingsPage from './view'; import SettingsPage from './view';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
@ -37,6 +37,7 @@ const select = (state) => ({
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
darkModeTimes: makeSelectClientSetting(SETTINGS.DARK_MODE_TIMES)(state), darkModeTimes: makeSelectClientSetting(SETTINGS.DARK_MODE_TIMES)(state),
language: selectLanguage(state), language: selectLanguage(state),
myChannelUrls: selectMyChannelUrls(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({

View file

@ -71,6 +71,7 @@ type Props = {
language?: string, language?: string,
enterSettings: () => void, enterSettings: () => void,
exitSettings: () => void, exitSettings: () => void,
myChannelUrls: ?Array<string>,
}; };
type State = { type State = {
@ -187,6 +188,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
darkModeTimes, darkModeTimes,
clearCache, clearCache,
openModal, openModal,
myChannelUrls,
} = this.props; } = this.props;
const { storedPassword } = this.state; const { storedPassword } = this.state;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0; const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
@ -468,6 +470,22 @@ class SettingsPage extends React.PureComponent<Props, State> {
} }
/> />
{myChannelUrls && myChannelUrls.length > 0 && (
<Card
title={__('Creator settings')}
actions={
<div className="section__actions">
<Button
button="secondary"
label={__('Manage')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.SETTINGS_CREATOR}`}
/>
</div>
}
/>
)}
<Card <Card
title={__('Advanced settings')} title={__('Advanced settings')}
actions={ actions={

View file

@ -0,0 +1,30 @@
import { connect } from 'react-redux';
import SettingsCreatorPage from './view';
import {
doCommentBlockWords,
doCommentUnblockWords,
doFetchCreatorSettings,
doUpdateCreatorSettings,
} from 'redux/actions/comments';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import {
selectSettingsByChannelId,
selectFetchingCreatorSettings,
selectFetchingBlockedWords,
} from 'redux/selectors/comments';
const select = (state) => ({
activeChannelClaim: selectActiveChannelClaim(state),
settingsByChannelId: selectSettingsByChannelId(state),
fetchingCreatorSettings: selectFetchingCreatorSettings(state),
fetchingBlockedWords: selectFetchingBlockedWords(state),
});
const perform = (dispatch) => ({
commentBlockWords: (channelClaim, words) => dispatch(doCommentBlockWords(channelClaim, words)),
commentUnblockWords: (channelClaim, words) => dispatch(doCommentUnblockWords(channelClaim, words)),
fetchCreatorSettings: (channelClaimIds) => dispatch(doFetchCreatorSettings(channelClaimIds)),
updateCreatorSettings: (channelClaim, settings) => dispatch(doUpdateCreatorSettings(channelClaim, settings)),
});
export default connect(select, perform)(SettingsCreatorPage);

View file

@ -0,0 +1,226 @@
// @flow
import * as React from 'react';
import Card from 'component/common/card';
import TagsSearch from 'component/tagsSearch';
import Page from 'component/page';
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';
const DEBOUNCE_REFRESH_MS = 1000;
type Props = {
activeChannelClaim: ChannelClaim,
settingsByChannelId: { [string]: PerChannelSettings },
fetchingCreatorSettings: boolean,
fetchingBlockedWords: boolean,
commentBlockWords: (ChannelClaim, Array<string>) => void,
commentUnblockWords: (ChannelClaim, Array<string>) => void,
fetchCreatorSettings: (Array<string>) => void,
updateCreatorSettings: (ChannelClaim, PerChannelSettings) => void,
};
export default function SettingsCreatorPage(props: Props) {
const {
activeChannelClaim,
settingsByChannelId,
commentBlockWords,
commentUnblockWords,
fetchCreatorSettings,
updateCreatorSettings,
} = props;
const [commentsEnabled, setCommentsEnabled] = React.useState(true);
const [mutedWordTags, setMutedWordTags] = 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());
}
// 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];
return (
<Page
noFooter
noSideNavigation
backout={{
title: __('Creator settings'),
backLabel: __('Done'),
}}
className="card-stack"
>
<ChannelSelector hideAnon />
{isBusy && (
<div className="main--empty">
<Spinner />
</div>
)}
{!isBusy && (
<>
<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 for Slow Mode in livestream chat.')}
min={0}
step={1}
type="number"
placeholder="1"
value={slowModeMinGap}
onChange={(e) => setSettings({ slow_mode_min_gap: 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
/>
</div>
}
/>
<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) })}
/>
</>
}
/>
</>
)}
</Page>
);
}

View file

@ -73,3 +73,15 @@
margin: calc(2rem / 10) calc(2rem / 10); margin: calc(2rem / 10) calc(2rem / 10);
max-width: 20rem; max-width: 20rem;
} }
.tag--blocked-words {
.tag {
color: var(--color-tag-words);
background-color: var(--color-tag-words-bg);
&:hover {
color: var(--color-tag-words-hover);
background-color: var(--color-tag-words-bg-hover);
}
}
}

View file

@ -115,6 +115,12 @@
--color-tag-hover: var(--color-primary-alt); --color-tag-hover: var(--color-primary-alt);
--color-tag-bg-hover: var(--color-button-primary-bg); --color-tag-bg-hover: var(--color-button-primary-bg);
// Tags (words)
--color-tag-words: var(--color-gray-5);
--color-tag-words-bg: var(--color-button-alt-bg);
--color-tag-words-hover: var(--color-button-alt-text);
--color-tag-words-bg-hover: var(--color-button-alt-bg-hover);
// Menu // Menu
--color-menu-background: var(--color-header-background); --color-menu-background: var(--color-header-background);
--color-menu-background--active: var(--color-card-background-highlighted); --color-menu-background--active: var(--color-card-background-highlighted);

View file

@ -26,6 +26,12 @@
--color-text-error: #f87171; --color-text-error: #f87171;
--color-error: #61373f; --color-error: #61373f;
// Tags (words)
--color-tag-words: var(--color-text);
--color-tag-words-bg: var(--color-gray-5);
--color-tag-words-hover: var(--color-white);
--color-tag-words-bg-hover: var(--color-gray-4);
// Header // Header
--color-header-background: var(--color-gray-8); --color-header-background: var(--color-gray-8);
--color-header-button: var(--color-gray-7); --color-header-button: var(--color-gray-7);