diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index 2ac88e1f1..822810b26 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -201,7 +201,7 @@ declare type ModerationBlockParams = { creator_channel_name?: string, // Blocks identity from comment universally, requires Admin rights on commentron instance block_all?: boolean, - time_out_hrs?: number, + time_out?: number, // If true will delete all comments of the offender, requires Admin rights on commentron for universal delete delete_all?: boolean, // The usual signature stuff diff --git a/package.json b/package.json index e7303aa87..04c3ec4a8 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "electron-updater": "^4.2.4", "express": "^4.17.1", "if-env": "^1.0.4", + "parse-duration": "^1.0.0", "react-datetime-picker": "^3.2.1", "react-plastic": "^1.1.1", "react-top-loading-bar": "^2.0.1", diff --git a/static/app-strings.json b/static/app-strings.json index 9ce061707..4f9e45e9d 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1744,6 +1744,11 @@ "Moderator Block": "Moderator Block", "Block this channel on behalf of %creator%": "Block this channel on behalf of %creator%", "creator": "creator", + "Enter the timeout duration. Examples: %examples%": "Enter the timeout duration. Examples: %examples%", + "Wow, banned for more than 100 years?": "Wow, banned for more than 100 years?", + "Invalid duration.": "Invalid duration.", + "Permanent": "Permanent", + "Timeout --[time-based ban instead of permanent]--": "Timeout", "Create a channel to change this setting.": "Create a channel to change this setting.", "Invalid channel URL \"%url%\"": "Invalid channel URL \"%url%\"", "Delegation": "Delegation", diff --git a/ui/modal/modalBlockChannel/view.jsx b/ui/modal/modalBlockChannel/view.jsx index 50de3ac7b..d15e7b9cf 100644 --- a/ui/modal/modalBlockChannel/view.jsx +++ b/ui/modal/modalBlockChannel/view.jsx @@ -1,11 +1,14 @@ // @flow import React from 'react'; import classnames from 'classnames'; +import parseDuration from 'parse-duration'; import Button from 'component/button'; import ChannelThumbnail from 'component/channelThumbnail'; import ClaimPreview from 'component/claimPreview'; import Card from 'component/common/card'; import { FormField } from 'component/common/form'; +import Icon from 'component/common/icon'; +import * as ICONS from 'constants/icons'; import usePersistedState from 'effects/use-persisted-state'; import { Modal } from 'modal/modal'; @@ -64,11 +67,12 @@ export default function ModalBlockChannel(props: Props) { const [tab, setTab] = usePersistedState('ModalBlockChannel:tab', TAB.PERSONAL); const [blockType, setBlockType] = usePersistedState('ModalBlockChannel:blockType', BLOCK.PERMANENT); - const [timeoutHrs, setTimeoutHrs] = usePersistedState('ModalBlockChannel:timeoutHrs', 1); - const [timeoutHrsError, setTimeoutHrsError] = React.useState(''); + const [timeoutInput, setTimeoutInput] = usePersistedState('ModalBlockChannel:timeoutInput', '10m'); + const [timeoutInputErr, setTimeoutInputErr] = React.useState(''); + const [timeoutSec, setTimeoutSec] = React.useState(-1); const personalIsTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin; - const blockButtonDisabled = blockType === BLOCK.TIMEOUT && (timeoutHrs === 0 || !Number.isInteger(timeoutHrs)); + const blockButtonDisabled = blockType === BLOCK.TIMEOUT && timeoutSec < 1; // ************************************************************************** // ************************************************************************** @@ -84,18 +88,39 @@ export default function ModalBlockChannel(props: Props) { } }, []); // eslint-disable-line react-hooks/exhaustive-deps - // 'timeoutHrs' sanity check. + // 'timeoutInput' to 'timeoutSec' conversion. React.useEffect(() => { - if (Number.isInteger(timeoutHrs) && timeoutHrs > 0) { - if (timeoutHrsError) { - setTimeoutHrsError(''); + const setInvalid = (errMsg: string) => { + if (timeoutSec !== -1) { + setTimeoutSec(-1); + } + if (!timeoutInputErr) { + setTimeoutInputErr(errMsg); + } + }; + + const setValid = (seconds) => { + if (seconds !== timeoutSec) { + setTimeoutSec(seconds); + } + if (timeoutInputErr) { + setTimeoutInputErr(''); + } + }; + + 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) { + setInvalid('Wow, banned for more than 100 years?'); + } else { + setValid(seconds); } } else { - if (!timeoutHrsError) { - setTimeoutHrsError('Invalid duration.'); - } + setInvalid('Invalid duration.'); } - }, [timeoutHrs, timeoutHrsError]); + }, [timeoutInput, timeoutInputErr, timeoutSec]); // ************************************************************************** // ************************************************************************** @@ -143,19 +168,27 @@ export default function ModalBlockChannel(props: Props) { } function getTimeoutDurationElem() { + const examples = '\n- 30s\n- 10m\n- 1h\n- 2d\n- 3mo\n- 1y'; return ( setTimeoutHrs(parseInt(e.target.value))} - error={timeoutHrsError} + name="time_out" + label={ + <> + {__('Duration')} + + + } + type="text" + placeholder="30s, 10m, 1h, 2d, 3mo, 1y" + value={timeoutInput} + onChange={(e) => setTimeoutInput(e.target.value)} + error={timeoutInputErr} /> ); } @@ -180,7 +213,7 @@ export default function ModalBlockChannel(props: Props) { } function handleBlock() { - const duration = blockType === BLOCK.TIMEOUT && timeoutHrs ? timeoutHrs : undefined; + const duration = blockType === BLOCK.TIMEOUT && timeoutSec > 0 ? timeoutSec : undefined; switch (tab) { case TAB.PERSONAL: @@ -232,7 +265,7 @@ export default function ModalBlockChannel(props: Props) {
{getBlockTypeElem(BLOCK.PERMANENT, 'Permanent')} - {getBlockTypeElem(BLOCK.TIMEOUT, 'Timeout')} + {getBlockTypeElem(BLOCK.TIMEOUT, 'Timeout --[time-based ban instead of permanent]--')}
{blockType === BLOCK.TIMEOUT && getTimeoutDurationElem()}
diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index 272f6048a..dd97f8301 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -738,7 +738,7 @@ function doCommentModToggleBlock( creatorId: string, blockerIds: Array, // [] = use all my channels blockLevel: string, - timeoutHours?: number, + timeoutSec?: number, showLink: boolean = false ) { return async (dispatch: Dispatch, getState: GetState) => { @@ -845,7 +845,7 @@ function doCommentModToggleBlock( block_all: unblock ? undefined : blockLevel === BLOCK_LEVEL.ADMIN, global_un_block: unblock ? blockLevel === BLOCK_LEVEL.ADMIN : undefined, ...sharedModBlockParams, - time_out_hrs: unblock ? undefined : timeoutHours, + time_out: unblock ? undefined : timeoutSec, }) ) ) diff --git a/yarn.lock b/yarn.lock index 2b521ec43..9b1e9da25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11916,6 +11916,11 @@ parse-asn1@^5.0.0: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-duration@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-1.0.0.tgz#8605651745f61088f6fb14045c887526c291858c" + integrity sha512-X4kUkCTHU1N/kEbwK9FpUJ0UZQa90VzeczfS704frR30gljxDG0pSziws06XlK+CGRSo/1wtG1mFIdBFQTMQNw== + parse-entities@^1.0.2, parse-entities@^1.1.0: version "1.2.2" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50"