diff --git a/ui/component/formFieldDuration/index.js b/ui/component/formFieldDuration/index.js new file mode 100644 index 000000000..d68437f88 --- /dev/null +++ b/ui/component/formFieldDuration/index.js @@ -0,0 +1,3 @@ +import FormFieldDuration from './view'; + +export default FormFieldDuration; diff --git a/ui/component/formFieldDuration/view.jsx b/ui/component/formFieldDuration/view.jsx new file mode 100644 index 000000000..2667d5a10 --- /dev/null +++ b/ui/component/formFieldDuration/view.jsx @@ -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 ( + + {label || __('Duration')} + + + } + placeholder={placeholder || '30s, 10m, 1h, 2d, 3mo, 1y'} + value={value} + onChange={onChange} + error={valueErr} + /> + ); +} diff --git a/ui/modal/modalBlockChannel/view.jsx b/ui/modal/modalBlockChannel/view.jsx index 8b1a514d0..a9c7fb010 100644 --- a/ui/modal/modalBlockChannel/view.jsx +++ b/ui/modal/modalBlockChannel/view.jsx @@ -1,14 +1,12 @@ // @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 FormFieldDuration from 'component/formFieldDuration'; import usePersistedState from 'effects/use-persisted-state'; import { Modal } from 'modal/modal'; import { getChannelFromClaim } from 'util/claim'; @@ -76,7 +74,6 @@ export default function ModalBlockChannel(props: Props) { const [tab, setTab] = usePersistedState('ModalBlockChannel:tab', TAB.PERSONAL); const [blockType, setBlockType] = usePersistedState('ModalBlockChannel:blockType', BLOCK.PERMANENT); const [timeoutInput, setTimeoutInput] = usePersistedState('ModalBlockChannel:timeoutInput', '10m'); - const [timeoutInputErr, setTimeoutInputErr] = React.useState(''); const [timeoutSec, setTimeoutSec] = React.useState(-1); const isPersonalTheOnlyTab = !activeChannelIsModerator && !activeChannelIsAdmin; @@ -101,45 +98,6 @@ export default function ModalBlockChannel(props: Props) { } }, []); // 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() { - const examples = '\n- 30s\n- 10m\n- 1h\n- 2d\n- 3mo\n- 1y'; return ( - - {__('Duration')} - - - } - type="text" - placeholder="30s, 10m, 1h, 2d, 3mo, 1y" value={timeoutInput} onChange={(e) => setTimeoutInput(e.target.value)} - error={timeoutInputErr} + onResolve={(valueInSeconds) => setTimeoutSec(valueInSeconds)} /> ); }