Comment: minimum channel age (#940)
This commit is contained in:
commit
d6cd3caa77
13 changed files with 250 additions and 61 deletions
2
flow-typed/Comment.js
vendored
2
flow-typed/Comment.js
vendored
|
@ -37,6 +37,7 @@ declare type PerChannelSettings = {
|
||||||
min_tip_amount_comment?: number,
|
min_tip_amount_comment?: number,
|
||||||
min_tip_amount_super_chat?: number,
|
min_tip_amount_super_chat?: number,
|
||||||
slow_mode_min_gap?: number,
|
slow_mode_min_gap?: number,
|
||||||
|
time_since_first_comment?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
// todo: relate individual comments to their commentId
|
// todo: relate individual comments to their commentId
|
||||||
|
@ -318,6 +319,7 @@ declare type UpdateSettingsParams = {
|
||||||
min_tip_amount_comment?: number,
|
min_tip_amount_comment?: number,
|
||||||
min_tip_amount_super_chat?: number,
|
min_tip_amount_super_chat?: number,
|
||||||
slow_mode_min_gap?: number,
|
slow_mode_min_gap?: number,
|
||||||
|
time_since_first_comment?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type BlockWordParams = {
|
declare type BlockWordParams = {
|
||||||
|
|
|
@ -1401,6 +1401,7 @@
|
||||||
"Unable to comment. Your channel has been blocked by an admin.": "Unable to comment. Your channel has been blocked by an admin.",
|
"Unable to comment. Your channel has been blocked by an admin.": "Unable to comment. Your channel has been blocked by an admin.",
|
||||||
"Unable to comment. The content owner has disabled comments.": "Unable to comment. The content owner has disabled comments.",
|
"Unable to comment. The content owner has disabled comments.": "Unable to comment. The content owner has disabled comments.",
|
||||||
"Please do not spam.": "Please do not spam.",
|
"Please do not spam.": "Please do not spam.",
|
||||||
|
"Your channel does not meet the creator's minimum channel-age limit.": "Your channel does not meet the creator's minimum channel-age limit.",
|
||||||
"Slow mode is on. Please wait up to %1% seconds before commenting again.": "Slow mode is on. Please wait up to %1% seconds before commenting again.",
|
"Slow mode is on. Please wait up to %1% seconds before commenting again.": "Slow mode is on. Please wait up to %1% seconds before commenting again.",
|
||||||
"The comment contains contents that are blocked by %1%.": "The comment contains contents that are blocked by %1%.",
|
"The comment contains contents that are blocked by %1%.": "The comment contains contents that are blocked by %1%.",
|
||||||
"Your user name \"%1%\" is too close to the creator's user name \"%2%\" and may cause confusion. Please use another identity.": "Your user name \"%1%\" is too close to the creator's user name \"%2%\" and may cause confusion. Please use another identity.",
|
"Your user name \"%1%\" is too close to the creator's user name \"%2%\" and may cause confusion. Please use another identity.": "Your user name \"%1%\" is too close to the creator's user name \"%2%\" and may cause confusion. Please use another identity.",
|
||||||
|
@ -2197,5 +2198,10 @@
|
||||||
"When do you want to go live?": "When do you want to go live?",
|
"When do you want to go live?": "When do you want to go live?",
|
||||||
"Anytime": "Anytime",
|
"Anytime": "Anytime",
|
||||||
"Scheduled Time": "Scheduled Time",
|
"Scheduled Time": "Scheduled Time",
|
||||||
|
"Minimum channel age for comments": "Minimum channel age for comments",
|
||||||
|
"Channels with a lifespan lower than the specified duration will not be able to comment on your content.": "Channels with a lifespan lower than the specified duration will not be able to comment on your content.",
|
||||||
|
"Set minimum channel age": "Set minimum channel age",
|
||||||
|
"The minimum duration must not exceed Feb 8th, 2022.": "The minimum duration must not exceed Feb 8th, 2022.",
|
||||||
|
"No limit": "No limit",
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
3
ui/component/formFieldDuration/index.js
Normal file
3
ui/component/formFieldDuration/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import FormFieldDuration from './view';
|
||||||
|
|
||||||
|
export default FormFieldDuration;
|
90
ui/component/formFieldDuration/view.jsx
Normal file
90
ui/component/formFieldDuration/view.jsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// @flow
|
||||||
|
import type { Node } from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
import parseDuration from 'parse-duration';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
|
||||||
|
const INPUT_EXAMPLES = '\n- 30s\n- 10m\n- 1h\n- 2d\n- 3mo\n- 1y';
|
||||||
|
const ONE_HUNDRED_YEARS_IN_SECONDS = 3154000000;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string,
|
||||||
|
label?: string | Node,
|
||||||
|
placeholder?: string | number,
|
||||||
|
disabled?: boolean,
|
||||||
|
value: string | number,
|
||||||
|
onChange: (any) => void,
|
||||||
|
onResolve: (valueInSeconds: number) => void, // Returns parsed/resolved value in seconds; "-1" for invalid input.
|
||||||
|
maxDurationInSeconds?: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FormFieldDuration(props: Props) {
|
||||||
|
const { name, label, placeholder, disabled, value, onChange, onResolve, maxDurationInSeconds } = props;
|
||||||
|
const [valueSec, setValueSec] = React.useState(-1);
|
||||||
|
const [valueErr, setValueErr] = React.useState('');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleInvalidInput = (errMsg: string) => {
|
||||||
|
if (valueSec !== -1) {
|
||||||
|
setValueSec(-1);
|
||||||
|
}
|
||||||
|
if (valueErr !== errMsg) {
|
||||||
|
setValueErr(errMsg);
|
||||||
|
}
|
||||||
|
onResolve(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValidInput = (seconds) => {
|
||||||
|
if (seconds !== valueSec) {
|
||||||
|
setValueSec(seconds);
|
||||||
|
onResolve(seconds);
|
||||||
|
}
|
||||||
|
if (valueErr) {
|
||||||
|
setValueErr('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
handleValidInput(-1); // Reset
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seconds = parseDuration(value, 's');
|
||||||
|
if (Number.isInteger(seconds) && seconds > 0) {
|
||||||
|
const max = maxDurationInSeconds || ONE_HUNDRED_YEARS_IN_SECONDS;
|
||||||
|
if (seconds > max) {
|
||||||
|
handleInvalidInput(__('Value exceeded maximum.'));
|
||||||
|
} else {
|
||||||
|
handleValidInput(seconds);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleInvalidInput(__('Invalid duration.'));
|
||||||
|
}
|
||||||
|
}, [value, valueSec, valueErr, maxDurationInSeconds, onResolve]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
name={name}
|
||||||
|
type="text"
|
||||||
|
disabled={disabled}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
{label || __('Duration')}
|
||||||
|
<Icon
|
||||||
|
customTooltipText={__('Examples: %examples%', { examples: INPUT_EXAMPLES })}
|
||||||
|
className="icon--help"
|
||||||
|
icon={ICONS.HELP}
|
||||||
|
tooltip
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
placeholder={placeholder || '30s, 10m, 1h, 2d, 3mo, 1y'}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
error={valueErr}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -42,6 +42,7 @@ export const IMAGE_UPLOAD = 'image_upload';
|
||||||
export const MOBILE_SEARCH = 'mobile_search';
|
export const MOBILE_SEARCH = 'mobile_search';
|
||||||
export const VIEW_IMAGE = 'view_image';
|
export const VIEW_IMAGE = 'view_image';
|
||||||
export const BLOCK_CHANNEL = 'block_channel';
|
export const BLOCK_CHANNEL = 'block_channel';
|
||||||
|
export const MIN_CHANNEL_AGE = 'min_channel_age';
|
||||||
export const COLLECTION_ADD = 'collection_add';
|
export const COLLECTION_ADD = 'collection_add';
|
||||||
export const COLLECTION_DELETE = 'collection_delete';
|
export const COLLECTION_DELETE = 'collection_delete';
|
||||||
export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD';
|
export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD';
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import parseDuration from 'parse-duration';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import ClaimPreview from 'component/claimPreview';
|
import ClaimPreview from 'component/claimPreview';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import Icon from 'component/common/icon';
|
import FormFieldDuration from 'component/formFieldDuration';
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import { Modal } from 'modal/modal';
|
import { Modal } from 'modal/modal';
|
||||||
import { getChannelFromClaim } from 'util/claim';
|
import { getChannelFromClaim } from 'util/claim';
|
||||||
|
@ -76,7 +74,6 @@ export default function ModalBlockChannel(props: Props) {
|
||||||
const [tab, setTab] = usePersistedState('ModalBlockChannel:tab', TAB.PERSONAL);
|
const [tab, setTab] = usePersistedState('ModalBlockChannel:tab', TAB.PERSONAL);
|
||||||
const [blockType, setBlockType] = usePersistedState('ModalBlockChannel:blockType', BLOCK.PERMANENT);
|
const [blockType, setBlockType] = usePersistedState('ModalBlockChannel:blockType', BLOCK.PERMANENT);
|
||||||
const [timeoutInput, setTimeoutInput] = usePersistedState('ModalBlockChannel:timeoutInput', '10m');
|
const [timeoutInput, setTimeoutInput] = usePersistedState('ModalBlockChannel:timeoutInput', '10m');
|
||||||
const [timeoutInputErr, setTimeoutInputErr] = React.useState('');
|
|
||||||
const [timeoutSec, setTimeoutSec] = React.useState(-1);
|
const [timeoutSec, setTimeoutSec] = React.useState(-1);
|
||||||
|
|
||||||
const isPersonalTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin;
|
const isPersonalTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin;
|
||||||
|
@ -101,45 +98,6 @@ export default function ModalBlockChannel(props: Props) {
|
||||||
}
|
}
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// 'timeoutInput' to 'timeoutSec' conversion.
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleInvalidInput = (errMsg: string) => {
|
|
||||||
if (timeoutSec !== -1) {
|
|
||||||
setTimeoutSec(-1);
|
|
||||||
}
|
|
||||||
if (timeoutInputErr !== errMsg) {
|
|
||||||
setTimeoutInputErr(errMsg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleValidInput = (seconds) => {
|
|
||||||
if (seconds !== timeoutSec) {
|
|
||||||
setTimeoutSec(seconds);
|
|
||||||
}
|
|
||||||
if (timeoutInputErr) {
|
|
||||||
setTimeoutInputErr('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!timeoutInput) {
|
|
||||||
handleValidInput(-1); // Reset
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ONE_HUNDRED_YEARS_IN_SECONDS = 3154000000;
|
|
||||||
const seconds = parseDuration(timeoutInput, 's');
|
|
||||||
|
|
||||||
if (Number.isInteger(seconds) && seconds > 0) {
|
|
||||||
if (seconds > ONE_HUNDRED_YEARS_IN_SECONDS) {
|
|
||||||
handleInvalidInput(__('Wow, banned for more than 100 years?'));
|
|
||||||
} else {
|
|
||||||
handleValidInput(seconds);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handleInvalidInput(__('Invalid duration.'));
|
|
||||||
}
|
|
||||||
}, [timeoutInput, timeoutInputErr, timeoutSec]);
|
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
|
@ -187,27 +145,12 @@ export default function ModalBlockChannel(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimeoutDurationElem() {
|
function getTimeoutDurationElem() {
|
||||||
const examples = '\n- 30s\n- 10m\n- 1h\n- 2d\n- 3mo\n- 1y';
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormFieldDuration
|
||||||
name="time_out"
|
name="time_out"
|
||||||
label={
|
|
||||||
<>
|
|
||||||
{__('Duration')}
|
|
||||||
<Icon
|
|
||||||
customTooltipText={__('Enter the timeout duration. Examples: %examples%', { examples })}
|
|
||||||
className="icon--help"
|
|
||||||
icon={ICONS.HELP}
|
|
||||||
tooltip
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
type="text"
|
|
||||||
placeholder="30s, 10m, 1h, 2d, 3mo, 1y"
|
|
||||||
value={timeoutInput}
|
value={timeoutInput}
|
||||||
onChange={(e) => setTimeoutInput(e.target.value)}
|
onChange={(e) => setTimeoutInput(e.target.value)}
|
||||||
error={timeoutInputErr}
|
onResolve={(valueInSeconds) => setTimeoutSec(valueInSeconds)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
9
ui/modal/modalMinChannelAge/index.js
Normal file
9
ui/modal/modalMinChannelAge/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ModalMinChannelAge from './view';
|
||||||
|
import { doHideModal } from 'redux/actions/app';
|
||||||
|
|
||||||
|
const perform = {
|
||||||
|
doHideModal,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, perform)(ModalMinChannelAge);
|
76
ui/modal/modalMinChannelAge/view.jsx
Normal file
76
ui/modal/modalMinChannelAge/view.jsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import { FormField } from 'component/common/form-components/form-field';
|
||||||
|
import FormFieldDuration from 'component/formFieldDuration';
|
||||||
|
import { Modal } from 'modal/modal';
|
||||||
|
|
||||||
|
const CHANNEL_AGE_LIMIT_MIN_DATE = new Date('February 8, 2022 00:00:00');
|
||||||
|
const LIMITATION_WARNING = 'The minimum duration must not exceed Feb 8th, 2022.';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onConfirm: (limitInMinutes: number, closeModal: () => void) => void,
|
||||||
|
doHideModal: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ModalMinChannelAge(props: Props) {
|
||||||
|
const { onConfirm, doHideModal } = props;
|
||||||
|
|
||||||
|
const [showLimitationWarning, setShowLimitationWarning] = React.useState('');
|
||||||
|
const [limitDisabled, setLimitDisabled] = React.useState(false);
|
||||||
|
const [minChannelAgeInput, setMinChannelAgeInput] = React.useState('');
|
||||||
|
const [minChannelAgeMinutes, setMinChannelAgeMinutes] = React.useState(-1);
|
||||||
|
const inputOk = limitDisabled || (minChannelAgeMinutes > 0 && !showLimitationWarning);
|
||||||
|
|
||||||
|
function handleOnClick() {
|
||||||
|
if (onConfirm) {
|
||||||
|
onConfirm(limitDisabled ? 0 : minChannelAgeMinutes, doHideModal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnInputResolved(valueInSeconds) {
|
||||||
|
if (valueInSeconds > 0) {
|
||||||
|
const minCreationDate = moment().subtract(valueInSeconds, 'seconds').toDate();
|
||||||
|
setShowLimitationWarning(minCreationDate.getTime() < CHANNEL_AGE_LIMIT_MIN_DATE.getTime());
|
||||||
|
setMinChannelAgeMinutes(Math.ceil(valueInSeconds / 60));
|
||||||
|
} else {
|
||||||
|
setShowLimitationWarning(false);
|
||||||
|
setMinChannelAgeMinutes(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen type="card" onAborted={doHideModal}>
|
||||||
|
<Card
|
||||||
|
title={__('Set minimum channel age')}
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<FormFieldDuration
|
||||||
|
name="time_since_first_comment"
|
||||||
|
value={minChannelAgeInput}
|
||||||
|
disabled={limitDisabled}
|
||||||
|
onChange={(e) => setMinChannelAgeInput(e.target.value)}
|
||||||
|
onResolve={handleOnInputResolved}
|
||||||
|
/>
|
||||||
|
{showLimitationWarning && !limitDisabled && <p className="help--warning">{__(LIMITATION_WARNING)}</p>}
|
||||||
|
<FormField
|
||||||
|
type="checkbox"
|
||||||
|
name="no_limit"
|
||||||
|
label={__('No limit')}
|
||||||
|
checked={limitDisabled}
|
||||||
|
onChange={() => setLimitDisabled(!limitDisabled)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button button="primary" label={__('OK')} onClick={handleOnClick} disabled={!inputOk} />
|
||||||
|
<Button button="link" label={__('Cancel')} onClick={doHideModal} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -45,6 +45,9 @@ const ModalImageUpload = lazyImport(() => import('modal/modalImageUpload' /* web
|
||||||
const ModalMassTipsUnlock = lazyImport(() =>
|
const ModalMassTipsUnlock = lazyImport(() =>
|
||||||
import('modal/modalMassTipUnlock' /* webpackChunkName: "modalMassTipUnlock" */)
|
import('modal/modalMassTipUnlock' /* webpackChunkName: "modalMassTipUnlock" */)
|
||||||
);
|
);
|
||||||
|
const ModalMinChannelAge = lazyImport(() =>
|
||||||
|
import('modal/modalMinChannelAge' /* webpackChunkName: "modalMinChannelAge" */)
|
||||||
|
);
|
||||||
const ModalMobileSearch = lazyImport(() =>
|
const ModalMobileSearch = lazyImport(() =>
|
||||||
import('modal/modalMobileSearch' /* webpackChunkName: "modalMobileSearch" */)
|
import('modal/modalMobileSearch' /* webpackChunkName: "modalMobileSearch" */)
|
||||||
);
|
);
|
||||||
|
@ -169,6 +172,8 @@ function getModal(id) {
|
||||||
return ModalMassTipsUnlock;
|
return ModalMassTipsUnlock;
|
||||||
case MODALS.BLOCK_CHANNEL:
|
case MODALS.BLOCK_CHANNEL:
|
||||||
return ModalBlockChannel;
|
return ModalBlockChannel;
|
||||||
|
case MODALS.MIN_CHANNEL_AGE:
|
||||||
|
return ModalMinChannelAge;
|
||||||
case MODALS.COLLECTION_ADD:
|
case MODALS.COLLECTION_ADD:
|
||||||
return ModalClaimCollectionAdd;
|
return ModalClaimCollectionAdd;
|
||||||
case MODALS.COLLECTION_DELETE:
|
case MODALS.COLLECTION_DELETE:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import SettingsCreatorPage from './view';
|
import SettingsCreatorPage from './view';
|
||||||
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import {
|
import {
|
||||||
doCommentBlockWords,
|
doCommentBlockWords,
|
||||||
doCommentUnblockWords,
|
doCommentUnblockWords,
|
||||||
|
@ -35,6 +36,7 @@ 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)),
|
||||||
|
doOpenModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(SettingsCreatorPage);
|
export default connect(select, perform)(SettingsCreatorPage);
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import humanizeDuration from 'humanize-duration';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import * as MODALS from 'constants/modal_types';
|
||||||
|
import Button from 'component/button';
|
||||||
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';
|
||||||
|
@ -36,6 +40,7 @@ type Props = {
|
||||||
fetchCreatorSettings: (channelId: string) => void,
|
fetchCreatorSettings: (channelId: string) => void,
|
||||||
updateCreatorSettings: (ChannelClaim, PerChannelSettings) => void,
|
updateCreatorSettings: (ChannelClaim, PerChannelSettings) => void,
|
||||||
doToast: ({ message: string }) => void,
|
doToast: ({ message: string }) => void,
|
||||||
|
doOpenModal: (id: string, {}) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingsCreatorPage(props: Props) {
|
export default function SettingsCreatorPage(props: Props) {
|
||||||
|
@ -50,6 +55,7 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
commentModListDelegates,
|
commentModListDelegates,
|
||||||
fetchCreatorSettings,
|
fetchCreatorSettings,
|
||||||
updateCreatorSettings,
|
updateCreatorSettings,
|
||||||
|
doOpenModal,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [commentsEnabled, setCommentsEnabled] = React.useState(true);
|
const [commentsEnabled, setCommentsEnabled] = React.useState(true);
|
||||||
|
@ -58,6 +64,7 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
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 [minChannelAgeMinutes, setMinChannelAgeMinutes] = React.useState(0);
|
||||||
const [lastUpdated, setLastUpdated] = React.useState(1);
|
const [lastUpdated, setLastUpdated] = React.useState(1);
|
||||||
|
|
||||||
const pushSlowModeMinDebounced = React.useMemo(() => debounce(pushSlowModeMin, 1000), []); // eslint-disable-line react-hooks/exhaustive-deps
|
const pushSlowModeMinDebounced = React.useMemo(() => debounce(pushSlowModeMin, 1000), []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
@ -91,6 +98,7 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
setMinTip(settings.min_tip_amount_comment || 0);
|
setMinTip(settings.min_tip_amount_comment || 0);
|
||||||
setMinSuper(settings.min_tip_amount_super_chat || 0);
|
setMinSuper(settings.min_tip_amount_super_chat || 0);
|
||||||
setSlowModeMin(settings.slow_mode_min_gap || 0);
|
setSlowModeMin(settings.slow_mode_min_gap || 0);
|
||||||
|
setMinChannelAgeMinutes(settings.time_since_first_comment || 0);
|
||||||
doSetMutedWordTags(settings.words || []);
|
doSetMutedWordTags(settings.words || []);
|
||||||
} else {
|
} else {
|
||||||
if (settings.comments_enabled !== undefined) {
|
if (settings.comments_enabled !== undefined) {
|
||||||
|
@ -105,6 +113,9 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
if (settings.slow_mode_min_gap !== undefined) {
|
if (settings.slow_mode_min_gap !== undefined) {
|
||||||
setSlowModeMin(settings.slow_mode_min_gap);
|
setSlowModeMin(settings.slow_mode_min_gap);
|
||||||
}
|
}
|
||||||
|
if (settings.time_since_first_comment) {
|
||||||
|
setMinChannelAgeMinutes(settings.time_since_first_comment);
|
||||||
|
}
|
||||||
if (settings.words) {
|
if (settings.words) {
|
||||||
doSetMutedWordTags(settings.words);
|
doSetMutedWordTags(settings.words);
|
||||||
}
|
}
|
||||||
|
@ -285,6 +296,41 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow title={__('Minimum channel age for comments')} subtitle={__(HELP.CHANNEL_AGE)}>
|
||||||
|
<div className="section__actions">
|
||||||
|
<FormField
|
||||||
|
name="time_since_first_comment"
|
||||||
|
className="form-field--copyable"
|
||||||
|
disabled={minChannelAgeMinutes <= 0}
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={
|
||||||
|
minChannelAgeMinutes > 0
|
||||||
|
? humanizeDuration(minChannelAgeMinutes * 60 * 1000, { round: true })
|
||||||
|
: __('No limit')
|
||||||
|
}
|
||||||
|
inputButton={
|
||||||
|
<Button
|
||||||
|
button="secondary"
|
||||||
|
icon={ICONS.EDIT}
|
||||||
|
title={__('Change')}
|
||||||
|
onClick={() => {
|
||||||
|
doOpenModal(MODALS.MIN_CHANNEL_AGE, {
|
||||||
|
onConfirm: (limitInMinutes: number, closeModal: () => void) => {
|
||||||
|
setMinChannelAgeMinutes(limitInMinutes);
|
||||||
|
updateCreatorSettings(activeChannelClaim, {
|
||||||
|
time_since_first_comment: limitInMinutes,
|
||||||
|
});
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={
|
title={
|
||||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
|
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
|
||||||
|
@ -386,6 +432,7 @@ export default function SettingsCreatorPage(props: Props) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const HELP = {
|
const HELP = {
|
||||||
SLOW_MODE: 'Minimum time gap in seconds between comments (affects livestream chat as well).',
|
SLOW_MODE: 'Minimum time gap in seconds between comments (affects livestream chat as well).',
|
||||||
|
CHANNEL_AGE: 'Channels with a lifespan lower than the specified duration will not be able to comment on your content.',
|
||||||
MIN_TIP: 'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.',
|
MIN_TIP: 'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.',
|
||||||
MIN_SUPER: '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.',
|
MIN_SUPER: '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.',
|
||||||
MIN_SUPER_OFF: '(This settings is not applicable if all comments require a tip.)',
|
MIN_SUPER_OFF: '(This settings is not applicable if all comments require a tip.)',
|
||||||
|
|
|
@ -340,7 +340,8 @@
|
||||||
//padding-left: var(--spacing-m);
|
//padding-left: var(--spacing-m);
|
||||||
|
|
||||||
.button,
|
.button,
|
||||||
.checkbox {
|
.checkbox,
|
||||||
|
.section__actions {
|
||||||
&:only-child {
|
&:only-child {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,10 @@ const ERR_MAP: CommentronErrorMap = {
|
||||||
commentron: 'duplicate comment!',
|
commentron: 'duplicate comment!',
|
||||||
replacement: 'Please do not spam.',
|
replacement: 'Please do not spam.',
|
||||||
},
|
},
|
||||||
|
CHANNEL_AGE: {
|
||||||
|
commentron: 'this creator has set minimum account age requirements that are not currently met',
|
||||||
|
replacement: "Your channel does not meet the creator's minimum channel-age limit.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolveCommentronError(commentronMsg: string) {
|
export function resolveCommentronError(commentronMsg: string) {
|
||||||
|
|
Loading…
Reference in a new issue