diff --git a/.eslintrc.js b/.eslintrc.js index 8394d98b9c..9e965791b0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,10 +4,12 @@ module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', + 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended', 'plugin:import/recommended', 'plugin:promise/recommended', 'plugin:jsdoc/recommended', + 'plugin:prettier/recommended', ], env: { @@ -61,20 +63,9 @@ module.exports = { }, rules: { - 'brace-style': 'warn', - 'comma-dangle': ['error', 'always-multiline'], - 'comma-spacing': [ - 'warn', - { - before: false, - after: true, - }, - ], - 'comma-style': ['warn', 'last'], 'consistent-return': 'error', 'dot-notation': 'error', eqeqeq: ['error', 'always', { 'null': 'ignore' }], - indent: ['warn', 2], 'jsx-quotes': ['error', 'prefer-single'], 'no-case-declarations': 'off', 'no-catch-shadow': 'error', @@ -94,7 +85,6 @@ module.exports = { { property: 'substr', message: 'Use .slice instead of .substr.' }, ], 'no-self-assign': 'off', - 'no-trailing-spaces': 'warn', 'no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': [ @@ -102,32 +92,18 @@ module.exports = { { vars: 'all', args: 'after-used', + destructuredArrayIgnorePattern: '^_', ignoreRestSiblings: true, }, ], - 'object-curly-spacing': ['error', 'always'], - 'padded-blocks': [ - 'error', - { - classes: 'always', - }, - ], - quotes: ['error', 'single'], - semi: 'error', 'valid-typeof': 'error', 'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }], 'react/jsx-boolean-value': 'error', - 'react/jsx-closing-bracket-location': ['error', 'line-aligned'], - 'react/jsx-curly-spacing': 'error', 'react/display-name': 'off', 'react/jsx-equals-spacing': 'error', - 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], - 'react/jsx-indent': ['error', 2], 'react/jsx-no-bind': 'error', 'react/jsx-no-target-blank': 'off', - 'react/jsx-tag-spacing': 'error', - 'react/jsx-wrap-multilines': 'error', 'react/no-deprecated': 'off', 'react/no-unknown-property': 'off', 'react/self-closing-comp': 'error', @@ -208,6 +184,9 @@ module.exports = { ], }, ], + 'import/no-amd': 'error', + 'import/no-commonjs': 'error', + 'import/no-import-module-exports': 'error', 'import/no-webpack-loader-syntax': 'error', 'promise/always-return': 'off', @@ -255,6 +234,7 @@ module.exports = { '*.config.js', '.*rc.js', 'ide-helper.js', + 'config/webpack/**/*', ], env: { @@ -264,6 +244,10 @@ module.exports = { parserOptions: { sourceType: 'script', }, + + rules: { + 'import/no-commonjs': 'off', + }, }, { files: [ @@ -275,17 +259,25 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', + 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended', 'plugin:import/recommended', 'plugin:import/typescript', 'plugin:promise/recommended', 'plugin:jsdoc/recommended', + 'plugin:prettier/recommended', ], rules: { '@typescript-eslint/no-explicit-any': 'off', 'jsdoc/require-jsdoc': 'off', + + // Those rules set stricter rules for TS files + // to enforce better practices when converting from JS + 'import/no-default-export': 'warn', + 'react/prefer-stateless-function': 'warn', + 'react/function-component-definition': ['error', { namedComponents: 'arrow-function' }], }, }, { @@ -298,5 +290,13 @@ module.exports = { jest: true, }, }, + { + files: [ + 'streaming/**/*', + ], + rules: { + 'import/no-commonjs': 'off', + }, + }, ], }; diff --git a/.prettierignore b/.prettierignore index 36ba57bfb5..af0411e9cc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -70,8 +70,6 @@ app/javascript/styles/mastodon/reset.scss # Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631 *.js *.jsx -*.ts -*.tsx # Ignore HTML till cleaned and included in CI *.html diff --git a/.prettierrc.js b/.prettierrc.js index 1d70813d51..af39b253f6 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,3 +1,4 @@ module.exports = { - singleQuote: true + singleQuote: true, + jsxSingleQuote: true } diff --git a/.rubocop.yml b/.rubocop.yml index 966a2a43db..ceade6e582 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -157,7 +157,7 @@ Metrics/MethodLength: - 'lib/mastodon/*_cli.rb' # Reason: -# https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength Metrics/ModuleLength: CountAsOne: [array, heredoc] diff --git a/app/javascript/core/mailer.js b/app/javascript/core/mailer.js index a4b6d54464..a2ad5e73ac 100644 --- a/app/javascript/core/mailer.js +++ b/app/javascript/core/mailer.js @@ -1,3 +1,3 @@ -require('../styles/mailer.scss'); +import '../styles/mailer.scss'; require.context('../icons'); diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js index 4fdda5c3e6..01b4157f8c 100644 --- a/app/javascript/core/public.js +++ b/app/javascript/core/public.js @@ -2,7 +2,7 @@ import 'packs/public-path'; -const { delegate } = require('@rails/ujs'); +import { delegate } from '@rails/ujs'; const getProfileAvatarAnimationHandler = (swapTo) => { //animate avatar gifs on the profile page when moused over diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index d578463a33..c6cf6704c3 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -3,7 +3,7 @@ import 'packs/public-path'; import escapeTextContentForBrowser from 'escape-html'; -const { delegate } = require('@rails/ujs'); +import { delegate } from '@rails/ujs'; import emojify from '../mastodon/features/emoji/emoji'; diff --git a/app/javascript/flavours/glitch/actions/app.ts b/app/javascript/flavours/glitch/actions/app.ts index 1fc4416090..46b2cdf93c 100644 --- a/app/javascript/flavours/glitch/actions/app.ts +++ b/app/javascript/flavours/glitch/actions/app.ts @@ -1,7 +1,8 @@ import { createAction } from '@reduxjs/toolkit'; +import type { LayoutType } from '../is_mobile'; type ChangeLayoutPayload = { - layout: 'mobile' | 'single-column' | 'multi-column'; + layout: LayoutType; }; export const changeLayout = createAction('APP_LAYOUT_CHANGE'); diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index f826753426..8fd9da405e 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,6 +1,6 @@ import api from '../api'; import { debounce } from 'lodash'; -import compareId from '../compare_id'; +import { compareId } from '../compare_id'; import { List as ImmutableList } from 'immutable'; export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 989bc41444..c044ea927f 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -13,7 +13,7 @@ import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from 'flavours/glitch/utils/html'; import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; -import compareId from 'flavours/glitch/compare_id'; +import { compareId } from 'flavours/glitch/compare_id'; import { requestNotificationPermission } from 'flavours/glitch/utils/notifications'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index dbf668ccbd..b4eb60ad4e 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -52,8 +52,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti /** * @param {function(Function, Function): void} fallback */ + const useFallback = fallback => { fallback(dispatch, () => { + // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); }); }; diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index bde96c504b..603759760b 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -2,7 +2,7 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; import api, { getLinks } from 'flavours/glitch/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from 'flavours/glitch/compare_id'; +import { compareId } from 'flavours/glitch/compare_id'; import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; import { toServerSideType } from 'flavours/glitch/utils/filters'; diff --git a/app/javascript/flavours/glitch/blurhash.ts b/app/javascript/flavours/glitch/blurhash.ts index cb1c3b2c82..dadf2b7f2c 100644 --- a/app/javascript/flavours/glitch/blurhash.ts +++ b/app/javascript/flavours/glitch/blurhash.ts @@ -98,9 +98,9 @@ export const decode83 = (str: string) => { }; export const intToRGB = (int: number) => ({ - r: Math.max(0, (int >> 16)), + r: Math.max(0, int >> 16), g: Math.max(0, (int >> 8) & 255), - b: Math.max(0, (int & 255)), + b: Math.max(0, int & 255), }); export const getAverageFromBlurhash = (blurhash: string) => { diff --git a/app/javascript/flavours/glitch/compare_id.ts b/app/javascript/flavours/glitch/compare_id.ts index ae4ac6f897..30b0572481 100644 --- a/app/javascript/flavours/glitch/compare_id.ts +++ b/app/javascript/flavours/glitch/compare_id.ts @@ -1,4 +1,4 @@ -export default function compareId (id1: string, id2: string) { +export function compareId(id1: string, id2: string) { if (id1 === id2) { return 0; } diff --git a/app/javascript/flavours/glitch/components/account.jsx b/app/javascript/flavours/glitch/components/account.jsx index 7b66d5a6ef..e9f04fcda7 100644 --- a/app/javascript/flavours/glitch/components/account.jsx +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -1,14 +1,14 @@ import React, { Fragment } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import Avatar from './avatar'; +import { Avatar } from './avatar'; import DisplayName from './display_name'; import Permalink from './permalink'; -import IconButton from './icon_button'; +import { IconButton } from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from 'flavours/glitch/initial_state'; -import RelativeTimestamp from './relative_timestamp'; +import { RelativeTimestamp } from './relative_timestamp'; import Skeleton from 'flavours/glitch/components/skeleton'; const messages = defineMessages({ diff --git a/app/javascript/flavours/glitch/components/animated_number.tsx b/app/javascript/flavours/glitch/components/animated_number.tsx index 1673ff41bb..f6c77d35ff 100644 --- a/app/javascript/flavours/glitch/components/animated_number.tsx +++ b/app/javascript/flavours/glitch/components/animated_number.tsx @@ -16,13 +16,10 @@ const obfuscatedCount = (count: number) => { type Props = { value: number; obfuscate?: boolean; -} -export const AnimatedNumber: React.FC = ({ - value, - obfuscate, -})=> { +}; +export const AnimatedNumber: React.FC = ({ value, obfuscate }) => { const [previousValue, setPreviousValue] = useState(value); - const [direction, setDirection] = useState<1|-1>(1); + const [direction, setDirection] = useState<1 | -1>(1); if (previousValue !== value) { setPreviousValue(value); @@ -30,29 +27,48 @@ export const AnimatedNumber: React.FC = ({ } const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]); - const willLeave = useCallback(() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), [direction]); + const willLeave = useCallback( + () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), + [direction] + ); if (reduceMotion) { - return obfuscate ? <>{obfuscatedCount(value)} : ; + return obfuscate ? ( + <>{obfuscatedCount(value)} + ) : ( + + ); } - const styles = [{ - key: `${value}`, - data: value, - style: { y: spring(0, { damping: 35, stiffness: 400 }) }, - }]; + const styles = [ + { + key: `${value}`, + data: value, + style: { y: spring(0, { damping: 35, stiffness: 400 }) }, + }, + ]; return ( - - {items => ( + + {(items) => ( {items.map(({ key, data, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : } + 0 ? 'absolute' : 'static', + transform: `translateY(${style.y * 100}%)`, + }} + > + {obfuscate ? obfuscatedCount(data) : } + ))} )} ); }; - -export default AnimatedNumber; diff --git a/app/javascript/flavours/glitch/components/attachment_list.jsx b/app/javascript/flavours/glitch/components/attachment_list.jsx index 68b80b19fe..92ba98126a 100644 --- a/app/javascript/flavours/glitch/components/attachment_list.jsx +++ b/app/javascript/flavours/glitch/components/attachment_list.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; diff --git a/app/javascript/flavours/glitch/components/avatar.tsx b/app/javascript/flavours/glitch/components/avatar.tsx index d6a9621462..1bd7739f5f 100644 --- a/app/javascript/flavours/glitch/components/avatar.tsx +++ b/app/javascript/flavours/glitch/components/avatar.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import classNames from 'classnames'; import { autoPlayGif } from 'flavours/glitch/initial_state'; -import { useHovering } from 'hooks/useHovering'; +import { useHovering } from 'flavours/glitch/hooks/useHovering'; import type { Account } from 'flavours/glitch/types/resources'; type Props = { @@ -10,7 +10,7 @@ type Props = { size: number; style?: React.CSSProperties; inline?: boolean; -} +}; export const Avatar: React.FC = ({ account, @@ -19,7 +19,8 @@ export const Avatar: React.FC = ({ inline = false, style: styleFromParent, }) => { - const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif); + const { hovering, handleMouseEnter, handleMouseLeave } = + useHovering(autoPlayGif); const style = { ...styleFromParent, @@ -29,12 +30,18 @@ export const Avatar: React.FC = ({ }; if (account) { - style.backgroundImage = `url(${account.get(hovering ? 'avatar' : 'avatar_static')})`; + style.backgroundImage = `url(${account.get( + hovering ? 'avatar' : 'avatar_static' + )})`; } return (
= ({ /> ); }; - -export default Avatar; diff --git a/app/javascript/flavours/glitch/components/blurhash.tsx b/app/javascript/flavours/glitch/components/blurhash.tsx index 6fec6e1ef7..7005136765 100644 --- a/app/javascript/flavours/glitch/components/blurhash.tsx +++ b/app/javascript/flavours/glitch/components/blurhash.tsx @@ -8,14 +8,14 @@ type Props = { dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched children?: never; [key: string]: any; -} -function Blurhash({ +}; +const Blurhash: React.FC = ({ hash, width = 32, height = width, dummy = false, ...canvasProps -}: Props) { +}) => { const canvasRef = useRef(null); useEffect(() => { @@ -40,6 +40,8 @@ function Blurhash({ return ( ); -} +}; -export default React.memo(Blurhash); +const MemoizedBlurhash = React.memo(Blurhash); + +export { MemoizedBlurhash as Blurhash }; diff --git a/app/javascript/flavours/glitch/components/column_back_button.jsx b/app/javascript/flavours/glitch/components/column_back_button.jsx index e9e2615cbb..9a3afe8b3d 100644 --- a/app/javascript/flavours/glitch/components/column_back_button.jsx +++ b/app/javascript/flavours/glitch/components/column_back_button.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; import { createPortal } from 'react-dom'; export default class ColumnBackButton extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx index 4df045b5fb..b27ead1c73 100644 --- a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx +++ b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; export default class ColumnBackButtonSlim extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/column_header.jsx b/app/javascript/flavours/glitch/components/column_header.jsx index 6fbe2955d7..bd26605954 100644 --- a/app/javascript/flavours/glitch/components/column_header.jsx +++ b/app/javascript/flavours/glitch/components/column_header.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, diff --git a/app/javascript/flavours/glitch/components/dismissable_banner.jsx b/app/javascript/flavours/glitch/components/dismissable_banner.jsx index 9b3faf6f27..2aed19b88f 100644 --- a/app/javascript/flavours/glitch/components/dismissable_banner.jsx +++ b/app/javascript/flavours/glitch/components/dismissable_banner.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import IconButton from './icon_button'; +import { IconButton } from './icon_button'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; import { bannerSettings } from 'flavours/glitch/settings'; diff --git a/app/javascript/flavours/glitch/components/domain.jsx b/app/javascript/flavours/glitch/components/domain.jsx deleted file mode 100644 index 85ebdbde93..0000000000 --- a/app/javascript/flavours/glitch/components/domain.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, -}); - -class Account extends ImmutablePureComponent { - - static propTypes = { - domain: PropTypes.string, - onUnblockDomain: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleDomainUnblock = () => { - this.props.onUnblockDomain(this.props.domain); - }; - - render () { - const { domain, intl } = this.props; - - return ( -
-
- - {domain} - - -
- -
-
-
- ); - } - -} - -export default injectIntl(Account); diff --git a/app/javascript/flavours/glitch/components/domain.tsx b/app/javascript/flavours/glitch/components/domain.tsx new file mode 100644 index 0000000000..af0fec35af --- /dev/null +++ b/app/javascript/flavours/glitch/components/domain.tsx @@ -0,0 +1,42 @@ +import React, { useCallback } from 'react'; +import { IconButton } from './icon_button'; +import { InjectedIntl, defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + unblockDomain: { + id: 'account.unblock_domain', + defaultMessage: 'Unblock domain {domain}', + }, +}); + +type Props = { + domain: string; + onUnblockDomain: (domain: string) => void; + intl: InjectedIntl; +}; +const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { + const handleDomainUnblock = useCallback(() => { + onUnblockDomain(domain); + }, [domain, onUnblockDomain]); + + return ( +
+
+ + {domain} + + +
+ +
+
+
+ ); +}; + +export const Domain = injectIntl(_Domain); diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx index 7fb75b59ea..3f17bc1299 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import IconButton from './icon_button'; +import { IconButton } from './icon_button'; import Overlay from 'react-overlays/Overlay'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx index 93dfed2f06..5315b812fc 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx +++ b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, injectIntl } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; import DropdownMenu from './containers/dropdown_menu_container'; import { connect } from 'react-redux'; import { openModal } from 'flavours/glitch/actions/modal'; -import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; +import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; import InlineAccount from 'flavours/glitch/components/inline_account'; const mapDispatchToProps = (dispatch, { statusId }) => ({ diff --git a/app/javascript/flavours/glitch/components/gifv.tsx b/app/javascript/flavours/glitch/components/gifv.tsx index 8968170c5f..72914ba741 100644 --- a/app/javascript/flavours/glitch/components/gifv.tsx +++ b/app/javascript/flavours/glitch/components/gifv.tsx @@ -8,7 +8,7 @@ type Props = { width: number; height: number; onClick?: () => void; -} +}; export const GIFV: React.FC = ({ src, @@ -17,19 +17,23 @@ export const GIFV: React.FC = ({ width, height, onClick, -})=> { +}) => { const [loading, setLoading] = useState(true); - const handleLoadedData: React.ReactEventHandler = useCallback(() => { - setLoading(false); - }, [setLoading]); + const handleLoadedData: React.ReactEventHandler = + useCallback(() => { + setLoading(false); + }, [setLoading]); - const handleClick: React.MouseEventHandler = useCallback((e) => { - if (onClick) { - e.stopPropagation(); - onClick(); - } - }, [onClick]); + const handleClick: React.MouseEventHandler = useCallback( + (e) => { + if (onClick) { + e.stopPropagation(); + onClick(); + } + }, + [onClick] + ); return (
@@ -64,5 +68,3 @@ export const GIFV: React.FC = ({
); }; - -export default GIFV; diff --git a/app/javascript/flavours/glitch/components/icon.tsx b/app/javascript/flavours/glitch/components/icon.tsx index d85fff6ef6..4eb948dc76 100644 --- a/app/javascript/flavours/glitch/components/icon.tsx +++ b/app/javascript/flavours/glitch/components/icon.tsx @@ -7,8 +7,15 @@ type Props = { fixedWidth?: boolean; children?: never; [key: string]: any; -} -export const Icon: React.FC = ({ id, className, fixedWidth, ...other }) => - ; - -export default Icon; +}; +export const Icon: React.FC = ({ + id, + className, + fixedWidth, + ...other +}) => ( + +); diff --git a/app/javascript/flavours/glitch/components/icon_button.tsx b/app/javascript/flavours/glitch/components/icon_button.tsx index 2bda4ddf34..80b20ae1b0 100644 --- a/app/javascript/flavours/glitch/components/icon_button.tsx +++ b/app/javascript/flavours/glitch/components/icon_button.tsx @@ -21,18 +21,17 @@ type Props = { animate: boolean; overlay: boolean; tabIndex: number; - label: string; + label?: string; counter?: number; obfuscateCount?: boolean; href?: string; ariaHidden: boolean; -} +}; type States = { - activate: boolean, - deactivate: boolean, -} -export default class IconButton extends React.PureComponent { - + activate: boolean; + deactivate: boolean; +}; +export class IconButton extends React.PureComponent { static defaultProps = { size: 18, active: false, @@ -48,7 +47,7 @@ export default class IconButton extends React.PureComponent { deactivate: false, }; - UNSAFE_componentWillReceiveProps (nextProps: Props) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { if (!nextProps.animate) return; if (this.props.active && !nextProps.active) { @@ -58,7 +57,7 @@ export default class IconButton extends React.PureComponent { } } - handleClick: React.MouseEventHandler = (e) => { + handleClick: React.MouseEventHandler = (e) => { e.preventDefault(); if (!this.props.disabled && this.props.onClick != null) { @@ -84,7 +83,7 @@ export default class IconButton extends React.PureComponent { } }; - render () { + render() { // Hack required for some icons which have an overriden size let containerSize = '1.28571429em'; if (this.props.style?.fontSize) { @@ -120,10 +119,7 @@ export default class IconButton extends React.PureComponent { ariaHidden, } = this.props; - const { - activate, - deactivate, - } = this.state; + const { activate, deactivate } = this.state; const classes = classNames(className, 'icon-button', { active, @@ -141,7 +137,12 @@ export default class IconButton extends React.PureComponent { let contents = ( - ); @@ -174,5 +175,4 @@ export default class IconButton extends React.PureComponent { ); } - } diff --git a/app/javascript/flavours/glitch/components/icon_with_badge.tsx b/app/javascript/flavours/glitch/components/icon_with_badge.tsx index 487bf326ad..bf86814c03 100644 --- a/app/javascript/flavours/glitch/components/icon_with_badge.tsx +++ b/app/javascript/flavours/glitch/components/icon_with_badge.tsx @@ -1,20 +1,25 @@ import React from 'react'; import { Icon } from './icon'; -const formatNumber = (num: number): number | string => num > 40 ? '40+' : num; +const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num); type Props = { id: string; count: number; issueBadge: boolean; className: string; -} -const IconWithBadge: React.FC = ({ id, count, issueBadge, className }) => ( +}; +export const IconWithBadge: React.FC = ({ + id, + count, + issueBadge, + className, +}) => ( - {count > 0 && {formatNumber(count)}} + {count > 0 && ( + {formatNumber(count)} + )} {issueBadge && } ); - -export default IconWithBadge; diff --git a/app/javascript/flavours/glitch/components/image.jsx b/app/javascript/flavours/glitch/components/image.jsx deleted file mode 100644 index 6e81ddf082..0000000000 --- a/app/javascript/flavours/glitch/components/image.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Blurhash from './blurhash'; -import classNames from 'classnames'; - -export default class Image extends React.PureComponent { - - static propTypes = { - src: PropTypes.string, - srcSet: PropTypes.string, - blurhash: PropTypes.string, - className: PropTypes.string, - }; - - state = { - loaded: false, - }; - - handleLoad = () => this.setState({ loaded: true }); - - render () { - const { src, srcSet, blurhash, className } = this.props; - const { loaded } = this.state; - - return ( -
- {blurhash && } - -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/image.tsx b/app/javascript/flavours/glitch/components/image.tsx new file mode 100644 index 0000000000..490543424a --- /dev/null +++ b/app/javascript/flavours/glitch/components/image.tsx @@ -0,0 +1,33 @@ +import React, { useCallback, useState } from 'react'; +import { Blurhash } from './blurhash'; +import classNames from 'classnames'; + +type Props = { + src: string; + srcSet?: string; + blurhash?: string; + className?: string; +}; + +export const Image: React.FC = ({ + src, + srcSet, + blurhash, + className, +}) => { + const [loaded, setLoaded] = useState(false); + + const handleLoad = useCallback(() => { + setLoaded(true); + }, [setLoaded]); + + return ( +
+ {blurhash && } + +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/inline_account.jsx b/app/javascript/flavours/glitch/components/inline_account.jsx index c04618d66e..eeb58b5533 100644 --- a/app/javascript/flavours/glitch/components/inline_account.jsx +++ b/app/javascript/flavours/glitch/components/inline_account.jsx @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { makeGetAccount } from 'flavours/glitch/selectors'; -import Avatar from 'flavours/glitch/components/avatar'; +import { Avatar } from 'flavours/glitch/components/avatar'; const makeMapStateToProps = () => { const getAccount = makeGetAccount(); diff --git a/app/javascript/flavours/glitch/components/load_gap.jsx b/app/javascript/flavours/glitch/components/load_gap.jsx index e70365d9ef..9c81df6323 100644 --- a/app/javascript/flavours/glitch/components/load_gap.jsx +++ b/app/javascript/flavours/glitch/components/load_gap.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, diff --git a/app/javascript/flavours/glitch/components/media_gallery.jsx b/app/javascript/flavours/glitch/components/media_gallery.jsx index 9bbde3b5e9..1b4ce05743 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.jsx +++ b/app/javascript/flavours/glitch/components/media_gallery.jsx @@ -2,12 +2,12 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { is } from 'immutable'; -import IconButton from './icon_button'; +import { IconButton } from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; import { debounce } from 'lodash'; -import Blurhash from 'flavours/glitch/components/blurhash'; +import { Blurhash } from 'flavours/glitch/components/blurhash'; const messages = defineMessages({ hidden: { diff --git a/app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx b/app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx deleted file mode 100644 index b440c6be2f..0000000000 --- a/app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -const NotSignedInIndicator = () => ( -
-
- -
-
-); - -export default NotSignedInIndicator; diff --git a/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx new file mode 100644 index 0000000000..53945d6a7a --- /dev/null +++ b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +export const NotSignedInIndicator: React.FC = () => ( +
+
+ +
+
+); diff --git a/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx index 9d1139a051..532eb4358c 100644 --- a/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx +++ b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx @@ -10,7 +10,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; import classNames from 'classnames'; const messages = defineMessages({ diff --git a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx index 7e02556749..e8c9611a34 100644 --- a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; import { connect } from 'react-redux'; import { FormattedMessage } from 'react-intl'; diff --git a/app/javascript/flavours/glitch/components/poll.jsx b/app/javascript/flavours/glitch/components/poll.jsx index fb37612d9c..df23919563 100644 --- a/app/javascript/flavours/glitch/components/poll.jsx +++ b/app/javascript/flavours/glitch/components/poll.jsx @@ -8,8 +8,8 @@ import Motion from 'flavours/glitch/features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import escapeTextContentForBrowser from 'escape-html'; import emojify from 'flavours/glitch/features/emoji/emoji'; -import RelativeTimestamp from './relative_timestamp'; -import Icon from 'flavours/glitch/components/icon'; +import { RelativeTimestamp } from './relative_timestamp'; +import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ closed: { diff --git a/app/javascript/flavours/glitch/components/radio_button.jsx b/app/javascript/flavours/glitch/components/radio_button.jsx deleted file mode 100644 index 0496fa2868..0000000000 --- a/app/javascript/flavours/glitch/components/radio_button.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class RadioButton extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - checked: PropTypes.bool, - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - label: PropTypes.node.isRequired, - }; - - render () { - const { name, value, checked, onChange, label } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/radio_button.tsx b/app/javascript/flavours/glitch/components/radio_button.tsx new file mode 100644 index 0000000000..829f471747 --- /dev/null +++ b/app/javascript/flavours/glitch/components/radio_button.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import classNames from 'classnames'; + +type Props = { + value: string; + checked: boolean; + name: string; + onChange: (event: React.ChangeEvent) => void; + label: React.ReactNode; +}; + +export const RadioButton: React.FC = ({ + name, + value, + checked, + onChange, + label, +}) => { + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/relative_timestamp.tsx b/app/javascript/flavours/glitch/components/relative_timestamp.tsx index 89cad06658..65d9d27cb2 100644 --- a/app/javascript/flavours/glitch/components/relative_timestamp.tsx +++ b/app/javascript/flavours/glitch/components/relative_timestamp.tsx @@ -4,20 +4,50 @@ import { injectIntl, defineMessages, InjectedIntl } from 'react-intl'; const messages = defineMessages({ today: { id: 'relative_time.today', defaultMessage: 'today' }, just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, - just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' }, + just_now_full: { + id: 'relative_time.full.just_now', + defaultMessage: 'just now', + }, seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, - seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' }, + seconds_full: { + id: 'relative_time.full.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} ago', + }, minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, - minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' }, + minutes_full: { + id: 'relative_time.full.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago', + }, hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, - hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' }, + hours_full: { + id: 'relative_time.full.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} ago', + }, days: { id: 'relative_time.days', defaultMessage: '{number}d' }, - days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' }, - moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, - seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, - minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, - hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, - days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, + days_full: { + id: 'relative_time.full.days', + defaultMessage: '{number, plural, one {# day} other {# days}} ago', + }, + moments_remaining: { + id: 'time_remaining.moments', + defaultMessage: 'Moments remaining', + }, + seconds_remaining: { + id: 'time_remaining.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} left', + }, + minutes_remaining: { + id: 'time_remaining.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} left', + }, + hours_remaining: { + id: 'time_remaining.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} left', + }, + days_remaining: { + id: 'time_remaining.days', + defaultMessage: '{number, plural, one {# day} other {# days}} left', + }, }); const dateFormatOptions = { @@ -36,8 +66,8 @@ const shortDateFormatOptions = { const SECOND = 1000; const MINUTE = 1000 * 60; -const HOUR = 1000 * 60 * 60; -const DAY = 1000 * 60 * 60 * 24; +const HOUR = 1000 * 60 * 60; +const DAY = 1000 * 60 * 60 * 24; const MAX_DELAY = 2147483647; @@ -57,20 +87,27 @@ const selectUnits = (delta: number) => { const getUnitDelay = (units: string) => { switch (units) { - case 'second': - return SECOND; - case 'minute': - return MINUTE; - case 'hour': - return HOUR; - case 'day': - return DAY; - default: - return MAX_DELAY; + case 'second': + return SECOND; + case 'minute': + return MINUTE; + case 'hour': + return HOUR; + case 'day': + return DAY; + default: + return MAX_DELAY; } }; -export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year: number, timeGiven: boolean, short?: boolean) => { +export const timeAgoString = ( + intl: InjectedIntl, + date: Date, + now: number, + year: number, + timeGiven: boolean, + short?: boolean +) => { const delta = now - date.getTime(); let relativeTime; @@ -78,27 +115,49 @@ export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year: if (delta < DAY && !timeGiven) { relativeTime = intl.formatMessage(messages.today); } else if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full); + relativeTime = intl.formatMessage( + short ? messages.just_now : messages.just_now_full + ); } else if (delta < 7 * DAY) { if (delta < MINUTE) { - relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) }); + relativeTime = intl.formatMessage( + short ? messages.seconds : messages.seconds_full, + { number: Math.floor(delta / SECOND) } + ); } else if (delta < HOUR) { - relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) }); + relativeTime = intl.formatMessage( + short ? messages.minutes : messages.minutes_full, + { number: Math.floor(delta / MINUTE) } + ); } else if (delta < DAY) { - relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) }); + relativeTime = intl.formatMessage( + short ? messages.hours : messages.hours_full, + { number: Math.floor(delta / HOUR) } + ); } else { - relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) }); + relativeTime = intl.formatMessage( + short ? messages.days : messages.days_full, + { number: Math.floor(delta / DAY) } + ); } } else if (date.getFullYear() === year) { relativeTime = intl.formatDate(date, shortDateFormatOptions); } else { - relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' }); + relativeTime = intl.formatDate(date, { + ...shortDateFormatOptions, + year: 'numeric', + }); } return relativeTime; }; -const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGiven = true) => { +const timeRemainingString = ( + intl: InjectedIntl, + date: Date, + now: number, + timeGiven = true +) => { const delta = date.getTime() - now; let relativeTime; @@ -108,13 +167,21 @@ const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGi } else if (delta < 10 * SECOND) { relativeTime = intl.formatMessage(messages.moments_remaining); } else if (delta < MINUTE) { - relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); + relativeTime = intl.formatMessage(messages.seconds_remaining, { + number: Math.floor(delta / SECOND), + }); } else if (delta < HOUR) { - relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) }); + relativeTime = intl.formatMessage(messages.minutes_remaining, { + number: Math.floor(delta / MINUTE), + }); } else if (delta < DAY) { - relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) }); + relativeTime = intl.formatMessage(messages.hours_remaining, { + number: Math.floor(delta / HOUR), + }); } else { - relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) }); + relativeTime = intl.formatMessage(messages.days_remaining, { + number: Math.floor(delta / DAY), + }); } return relativeTime; @@ -126,78 +193,88 @@ type Props = { year: number; futureDate?: boolean; short?: boolean; -} +}; type States = { now: number; -} +}; class RelativeTimestamp extends React.Component { - state = { now: this.props.intl.now(), }; static defaultProps = { - year: (new Date()).getFullYear(), + year: new Date().getFullYear(), short: true, }; _timer: number | undefined; - shouldComponentUpdate (nextProps: Props, nextState: States) { + shouldComponentUpdate(nextProps: Props, nextState: States) { // As of right now the locale doesn't change without a new page load, // but we might as well check in case that ever changes. - return this.props.timestamp !== nextProps.timestamp || + return ( + this.props.timestamp !== nextProps.timestamp || this.props.intl.locale !== nextProps.intl.locale || - this.state.now !== nextState.now; + this.state.now !== nextState.now + ); } - UNSAFE_componentWillReceiveProps (nextProps: Props) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { if (this.props.timestamp !== nextProps.timestamp) { this.setState({ now: this.props.intl.now() }); } } - componentDidMount () { + componentDidMount() { this._scheduleNextUpdate(this.props, this.state); } - UNSAFE_componentWillUpdate (nextProps: Props, nextState: States) { + UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) { this._scheduleNextUpdate(nextProps, nextState); } - componentWillUnmount () { + componentWillUnmount() { window.clearTimeout(this._timer); } - _scheduleNextUpdate (props: Props, state: States) { + _scheduleNextUpdate(props: Props, state: States) { window.clearTimeout(this._timer); - const { timestamp } = props; - const delta = (new Date(timestamp)).getTime() - state.now; - const unitDelay = getUnitDelay(selectUnits(delta)); - const unitRemainder = Math.abs(delta % unitDelay); + const { timestamp } = props; + const delta = new Date(timestamp).getTime() - state.now; + const unitDelay = getUnitDelay(selectUnits(delta)); + const unitRemainder = Math.abs(delta % unitDelay); const updateInterval = 1000 * 10; - const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder); + const delay = + delta < 0 + ? Math.max(updateInterval, unitDelay - unitRemainder) + : Math.max(updateInterval, unitRemainder); this._timer = window.setTimeout(() => { this.setState({ now: this.props.intl.now() }); }, delay); } - render () { + render() { const { timestamp, intl, year, futureDate, short } = this.props; - const timeGiven = timestamp.includes('T'); - const date = new Date(timestamp); - const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short); + const timeGiven = timestamp.includes('T'); + const date = new Date(timestamp); + const relativeTime = futureDate + ? timeRemainingString(intl, date, this.state.now, timeGiven) + : timeAgoString(intl, date, this.state.now, year, timeGiven, short); return ( -
); }; - -export default Avatar; diff --git a/app/javascript/mastodon/components/avatar_composite.jsx b/app/javascript/mastodon/components/avatar_composite.jsx index 220bf5b4f8..e1fae95dc0 100644 --- a/app/javascript/mastodon/components/avatar_composite.jsx +++ b/app/javascript/mastodon/components/avatar_composite.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { autoPlayGif } from '../initial_state'; -import Avatar from './avatar'; +import { Avatar } from './avatar'; export default class AvatarComposite extends React.PureComponent { diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx index 5c65a928c5..1dbd533230 100644 --- a/app/javascript/mastodon/components/avatar_overlay.tsx +++ b/app/javascript/mastodon/components/avatar_overlay.tsx @@ -18,13 +18,19 @@ export const AvatarOverlay: React.FC = ({ baseSize = 36, overlaySize = 24, }) => { - const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif); - const accountSrc = hovering ? account?.get('avatar') : account?.get('avatar_static'); - const friendSrc = hovering ? friend?.get('avatar') : friend?.get('avatar_static'); + const { hovering, handleMouseEnter, handleMouseLeave } = + useHovering(autoPlayGif); + const accountSrc = hovering + ? account?.get('avatar') + : account?.get('avatar_static'); + const friendSrc = hovering + ? friend?.get('avatar') + : friend?.get('avatar_static'); return (
@@ -47,5 +53,3 @@ export const AvatarOverlay: React.FC = ({
); }; - -export default AvatarOverlay; diff --git a/app/javascript/mastodon/components/blurhash.tsx b/app/javascript/mastodon/components/blurhash.tsx index 6fec6e1ef7..7005136765 100644 --- a/app/javascript/mastodon/components/blurhash.tsx +++ b/app/javascript/mastodon/components/blurhash.tsx @@ -8,14 +8,14 @@ type Props = { dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched children?: never; [key: string]: any; -} -function Blurhash({ +}; +const Blurhash: React.FC = ({ hash, width = 32, height = width, dummy = false, ...canvasProps -}: Props) { +}) => { const canvasRef = useRef(null); useEffect(() => { @@ -40,6 +40,8 @@ function Blurhash({ return ( ); -} +}; -export default React.memo(Blurhash); +const MemoizedBlurhash = React.memo(Blurhash); + +export { MemoizedBlurhash as Blurhash }; diff --git a/app/javascript/mastodon/components/check.jsx b/app/javascript/mastodon/components/check.jsx deleted file mode 100644 index 2fd0af7401..0000000000 --- a/app/javascript/mastodon/components/check.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -const Check = () => ( - - - -); - -export default Check; diff --git a/app/javascript/mastodon/components/check.tsx b/app/javascript/mastodon/components/check.tsx new file mode 100644 index 0000000000..73d65595ea --- /dev/null +++ b/app/javascript/mastodon/components/check.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +export const Check: React.FC = () => ( + + + +); diff --git a/app/javascript/mastodon/components/column_back_button.jsx b/app/javascript/mastodon/components/column_back_button.jsx index faa800b2ad..19e2cb3637 100644 --- a/app/javascript/mastodon/components/column_back_button.jsx +++ b/app/javascript/mastodon/components/column_back_button.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import Icon from 'mastodon/components/icon'; +import { Icon } from 'mastodon/components/icon'; import { createPortal } from 'react-dom'; export default class ColumnBackButton extends React.PureComponent { diff --git a/app/javascript/mastodon/components/column_back_button_slim.jsx b/app/javascript/mastodon/components/column_back_button_slim.jsx index 46ac23736b..f2c8642eae 100644 --- a/app/javascript/mastodon/components/column_back_button_slim.jsx +++ b/app/javascript/mastodon/components/column_back_button_slim.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import ColumnBackButton from './column_back_button'; -import Icon from 'mastodon/components/icon'; +import { Icon } from 'mastodon/components/icon'; export default class ColumnBackButtonSlim extends ColumnBackButton { diff --git a/app/javascript/mastodon/components/column_header.jsx b/app/javascript/mastodon/components/column_header.jsx index afc526f27f..794ea9738a 100644 --- a/app/javascript/mastodon/components/column_header.jsx +++ b/app/javascript/mastodon/components/column_header.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; -import Icon from 'mastodon/components/icon'; +import { Icon } from 'mastodon/components/icon'; const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, diff --git a/app/javascript/mastodon/components/dismissable_banner.jsx b/app/javascript/mastodon/components/dismissable_banner.jsx index 242021e764..c433d95324 100644 --- a/app/javascript/mastodon/components/dismissable_banner.jsx +++ b/app/javascript/mastodon/components/dismissable_banner.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import IconButton from './icon_button'; +import { IconButton } from './icon_button'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; import { bannerSettings } from 'mastodon/settings'; diff --git a/app/javascript/mastodon/components/domain.jsx b/app/javascript/mastodon/components/domain.jsx deleted file mode 100644 index 85ebdbde93..0000000000 --- a/app/javascript/mastodon/components/domain.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, -}); - -class Account extends ImmutablePureComponent { - - static propTypes = { - domain: PropTypes.string, - onUnblockDomain: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleDomainUnblock = () => { - this.props.onUnblockDomain(this.props.domain); - }; - - render () { - const { domain, intl } = this.props; - - return ( -
-
- - {domain} - - -
- -
-
-
- ); - } - -} - -export default injectIntl(Account); diff --git a/app/javascript/mastodon/components/domain.tsx b/app/javascript/mastodon/components/domain.tsx new file mode 100644 index 0000000000..af0fec35af --- /dev/null +++ b/app/javascript/mastodon/components/domain.tsx @@ -0,0 +1,42 @@ +import React, { useCallback } from 'react'; +import { IconButton } from './icon_button'; +import { InjectedIntl, defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + unblockDomain: { + id: 'account.unblock_domain', + defaultMessage: 'Unblock domain {domain}', + }, +}); + +type Props = { + domain: string; + onUnblockDomain: (domain: string) => void; + intl: InjectedIntl; +}; +const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { + const handleDomainUnblock = useCallback(() => { + onUnblockDomain(domain); + }, [domain, onUnblockDomain]); + + return ( +
+
+ + {domain} + + +
+ +
+
+
+ ); +}; + +export const Domain = injectIntl(_Domain); diff --git a/app/javascript/mastodon/components/dropdown_menu.jsx b/app/javascript/mastodon/components/dropdown_menu.jsx index eaaa72fd83..0ed3e904f5 100644 --- a/app/javascript/mastodon/components/dropdown_menu.jsx +++ b/app/javascript/mastodon/components/dropdown_menu.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import IconButton from './icon_button'; +import { IconButton } from './icon_button'; import Overlay from 'react-overlays/Overlay'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; diff --git a/app/javascript/mastodon/components/edited_timestamp/index.jsx b/app/javascript/mastodon/components/edited_timestamp/index.jsx index 8a5417a7d9..745d8a4b6f 100644 --- a/app/javascript/mastodon/components/edited_timestamp/index.jsx +++ b/app/javascript/mastodon/components/edited_timestamp/index.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, injectIntl } from 'react-intl'; -import Icon from 'mastodon/components/icon'; +import { Icon } from 'mastodon/components/icon'; import DropdownMenu from './containers/dropdown_menu_container'; import { connect } from 'react-redux'; import { openModal } from 'mastodon/actions/modal'; -import RelativeTimestamp from 'mastodon/components/relative_timestamp'; +import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import InlineAccount from 'mastodon/components/inline_account'; const mapDispatchToProps = (dispatch, { statusId }) => ({ diff --git a/app/javascript/mastodon/components/gifv.tsx b/app/javascript/mastodon/components/gifv.tsx index 8968170c5f..72914ba741 100644 --- a/app/javascript/mastodon/components/gifv.tsx +++ b/app/javascript/mastodon/components/gifv.tsx @@ -8,7 +8,7 @@ type Props = { width: number; height: number; onClick?: () => void; -} +}; export const GIFV: React.FC = ({ src, @@ -17,19 +17,23 @@ export const GIFV: React.FC = ({ width, height, onClick, -})=> { +}) => { const [loading, setLoading] = useState(true); - const handleLoadedData: React.ReactEventHandler = useCallback(() => { - setLoading(false); - }, [setLoading]); + const handleLoadedData: React.ReactEventHandler = + useCallback(() => { + setLoading(false); + }, [setLoading]); - const handleClick: React.MouseEventHandler = useCallback((e) => { - if (onClick) { - e.stopPropagation(); - onClick(); - } - }, [onClick]); + const handleClick: React.MouseEventHandler = useCallback( + (e) => { + if (onClick) { + e.stopPropagation(); + onClick(); + } + }, + [onClick] + ); return (
@@ -64,5 +68,3 @@ export const GIFV: React.FC = ({
); }; - -export default GIFV; diff --git a/app/javascript/mastodon/components/hashtag.jsx b/app/javascript/mastodon/components/hashtag.jsx index 254fae2fe0..d03b1a45a7 100644 --- a/app/javascript/mastodon/components/hashtag.jsx +++ b/app/javascript/mastodon/components/hashtag.jsx @@ -5,9 +5,7 @@ import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Link } from 'react-router-dom'; -// @ts-expect-error import ShortNumber from 'mastodon/components/short_number'; -// @ts-expect-error import Skeleton from 'mastodon/components/skeleton'; import classNames from 'classnames'; diff --git a/app/javascript/mastodon/components/icon.tsx b/app/javascript/mastodon/components/icon.tsx index d85fff6ef6..4eb948dc76 100644 --- a/app/javascript/mastodon/components/icon.tsx +++ b/app/javascript/mastodon/components/icon.tsx @@ -7,8 +7,15 @@ type Props = { fixedWidth?: boolean; children?: never; [key: string]: any; -} -export const Icon: React.FC = ({ id, className, fixedWidth, ...other }) => - ; - -export default Icon; +}; +export const Icon: React.FC = ({ + id, + className, + fixedWidth, + ...other +}) => ( + +); diff --git a/app/javascript/mastodon/components/icon_button.tsx b/app/javascript/mastodon/components/icon_button.tsx index ec11ab7011..1786414009 100644 --- a/app/javascript/mastodon/components/icon_button.tsx +++ b/app/javascript/mastodon/components/icon_button.tsx @@ -25,13 +25,12 @@ type Props = { obfuscateCount?: boolean; href?: string; ariaHidden: boolean; -} +}; type States = { - activate: boolean, - deactivate: boolean, -} -export default class IconButton extends React.PureComponent { - + activate: boolean; + deactivate: boolean; +}; +export class IconButton extends React.PureComponent { static defaultProps = { size: 18, active: false, @@ -47,7 +46,7 @@ export default class IconButton extends React.PureComponent { deactivate: false, }; - UNSAFE_componentWillReceiveProps (nextProps: Props) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { if (!nextProps.animate) return; if (this.props.active && !nextProps.active) { @@ -57,7 +56,7 @@ export default class IconButton extends React.PureComponent { } } - handleClick: React.MouseEventHandler = (e) => { + handleClick: React.MouseEventHandler = (e) => { e.preventDefault(); if (!this.props.disabled && this.props.onClick != null) { @@ -83,7 +82,7 @@ export default class IconButton extends React.PureComponent { } }; - render () { + render() { const style = { fontSize: `${this.props.size}px`, width: `${this.props.size * 1.28571429}px`, @@ -109,10 +108,7 @@ export default class IconButton extends React.PureComponent { ariaHidden, } = this.props; - const { - activate, - deactivate, - } = this.state; + const { activate, deactivate } = this.state; const classes = classNames(className, 'icon-button', { active, @@ -130,7 +126,12 @@ export default class IconButton extends React.PureComponent { let contents = ( - ); @@ -162,5 +163,4 @@ export default class IconButton extends React.PureComponent { ); } - } diff --git a/app/javascript/mastodon/components/icon_with_badge.tsx b/app/javascript/mastodon/components/icon_with_badge.tsx index 487bf326ad..bf86814c03 100644 --- a/app/javascript/mastodon/components/icon_with_badge.tsx +++ b/app/javascript/mastodon/components/icon_with_badge.tsx @@ -1,20 +1,25 @@ import React from 'react'; import { Icon } from './icon'; -const formatNumber = (num: number): number | string => num > 40 ? '40+' : num; +const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num); type Props = { id: string; count: number; issueBadge: boolean; className: string; -} -const IconWithBadge: React.FC = ({ id, count, issueBadge, className }) => ( +}; +export const IconWithBadge: React.FC = ({ + id, + count, + issueBadge, + className, +}) => ( - {count > 0 && {formatNumber(count)}} + {count > 0 && ( + {formatNumber(count)} + )} {issueBadge && } ); - -export default IconWithBadge; diff --git a/app/javascript/mastodon/components/image.jsx b/app/javascript/mastodon/components/image.jsx deleted file mode 100644 index 6e81ddf082..0000000000 --- a/app/javascript/mastodon/components/image.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Blurhash from './blurhash'; -import classNames from 'classnames'; - -export default class Image extends React.PureComponent { - - static propTypes = { - src: PropTypes.string, - srcSet: PropTypes.string, - blurhash: PropTypes.string, - className: PropTypes.string, - }; - - state = { - loaded: false, - }; - - handleLoad = () => this.setState({ loaded: true }); - - render () { - const { src, srcSet, blurhash, className } = this.props; - const { loaded } = this.state; - - return ( -
- {blurhash && } - -
- ); - } - -} diff --git a/app/javascript/mastodon/components/image.tsx b/app/javascript/mastodon/components/image.tsx new file mode 100644 index 0000000000..490543424a --- /dev/null +++ b/app/javascript/mastodon/components/image.tsx @@ -0,0 +1,33 @@ +import React, { useCallback, useState } from 'react'; +import { Blurhash } from './blurhash'; +import classNames from 'classnames'; + +type Props = { + src: string; + srcSet?: string; + blurhash?: string; + className?: string; +}; + +export const Image: React.FC = ({ + src, + srcSet, + blurhash, + className, +}) => { + const [loaded, setLoaded] = useState(false); + + const handleLoad = useCallback(() => { + setLoaded(true); + }, [setLoaded]); + + return ( +
+ {blurhash && } + +
+ ); +}; diff --git a/app/javascript/mastodon/components/inline_account.jsx b/app/javascript/mastodon/components/inline_account.jsx index 31dc63f93f..ca11f8b1bc 100644 --- a/app/javascript/mastodon/components/inline_account.jsx +++ b/app/javascript/mastodon/components/inline_account.jsx @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { makeGetAccount } from 'mastodon/selectors'; -import Avatar from 'mastodon/components/avatar'; +import { Avatar } from 'mastodon/components/avatar'; const makeMapStateToProps = () => { const getAccount = makeGetAccount(); diff --git a/app/javascript/mastodon/components/load_gap.jsx b/app/javascript/mastodon/components/load_gap.jsx index 2c91d37be7..2637bdbbc4 100644 --- a/app/javascript/mastodon/components/load_gap.jsx +++ b/app/javascript/mastodon/components/load_gap.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; -import Icon from 'mastodon/components/icon'; +import { Icon } from 'mastodon/components/icon'; const messages = defineMessages({ load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx index 54470f9402..54b414de20 100644 --- a/app/javascript/mastodon/components/media_gallery.jsx +++ b/app/javascript/mastodon/components/media_gallery.jsx @@ -2,12 +2,12 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { is } from 'immutable'; -import IconButton from './icon_button'; +import { IconButton } from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state'; import { debounce } from 'lodash'; -import Blurhash from 'mastodon/components/blurhash'; +import { Blurhash } from 'mastodon/components/blurhash'; const messages = defineMessages({ toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' }, diff --git a/app/javascript/mastodon/components/not_signed_in_indicator.jsx b/app/javascript/mastodon/components/not_signed_in_indicator.jsx deleted file mode 100644 index b440c6be2f..0000000000 --- a/app/javascript/mastodon/components/not_signed_in_indicator.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -const NotSignedInIndicator = () => ( -
-
- -
-
-); - -export default NotSignedInIndicator; diff --git a/app/javascript/mastodon/components/not_signed_in_indicator.tsx b/app/javascript/mastodon/components/not_signed_in_indicator.tsx new file mode 100644 index 0000000000..53945d6a7a --- /dev/null +++ b/app/javascript/mastodon/components/not_signed_in_indicator.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +export const NotSignedInIndicator: React.FC = () => ( +
+
+ +
+
+); diff --git a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx b/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx index a51c974017..4763a28d10 100644 --- a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Icon from 'mastodon/components/icon'; +import { Icon } from 'mastodon/components/icon'; import { removePictureInPicture } from 'mastodon/actions/picture_in_picture'; import { connect } from 'react-redux'; import { FormattedMessage } from 'react-intl'; diff --git a/app/javascript/mastodon/components/poll.jsx b/app/javascript/mastodon/components/poll.jsx index b9b96a7005..0ccdf472eb 100644 --- a/app/javascript/mastodon/components/poll.jsx +++ b/app/javascript/mastodon/components/poll.jsx @@ -8,8 +8,8 @@ import Motion from 'mastodon/features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import escapeTextContentForBrowser from 'escape-html'; import emojify from 'mastodon/features/emoji/emoji'; -import RelativeTimestamp from './relative_timestamp'; -import Icon from 'mastodon/components/icon'; +import { RelativeTimestamp } from './relative_timestamp'; +import { Icon } from 'mastodon/components/icon'; const messages = defineMessages({ closed: { diff --git a/app/javascript/mastodon/components/radio_button.jsx b/app/javascript/mastodon/components/radio_button.jsx deleted file mode 100644 index 0496fa2868..0000000000 --- a/app/javascript/mastodon/components/radio_button.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class RadioButton extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - checked: PropTypes.bool, - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - label: PropTypes.node.isRequired, - }; - - render () { - const { name, value, checked, onChange, label } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/mastodon/components/radio_button.tsx b/app/javascript/mastodon/components/radio_button.tsx new file mode 100644 index 0000000000..829f471747 --- /dev/null +++ b/app/javascript/mastodon/components/radio_button.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import classNames from 'classnames'; + +type Props = { + value: string; + checked: boolean; + name: string; + onChange: (event: React.ChangeEvent) => void; + label: React.ReactNode; +}; + +export const RadioButton: React.FC = ({ + name, + value, + checked, + onChange, + label, +}) => { + return ( + + ); +}; diff --git a/app/javascript/mastodon/components/relative_timestamp.tsx b/app/javascript/mastodon/components/relative_timestamp.tsx index 89cad06658..65d9d27cb2 100644 --- a/app/javascript/mastodon/components/relative_timestamp.tsx +++ b/app/javascript/mastodon/components/relative_timestamp.tsx @@ -4,20 +4,50 @@ import { injectIntl, defineMessages, InjectedIntl } from 'react-intl'; const messages = defineMessages({ today: { id: 'relative_time.today', defaultMessage: 'today' }, just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, - just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' }, + just_now_full: { + id: 'relative_time.full.just_now', + defaultMessage: 'just now', + }, seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, - seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' }, + seconds_full: { + id: 'relative_time.full.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} ago', + }, minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, - minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' }, + minutes_full: { + id: 'relative_time.full.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago', + }, hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, - hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' }, + hours_full: { + id: 'relative_time.full.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} ago', + }, days: { id: 'relative_time.days', defaultMessage: '{number}d' }, - days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' }, - moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, - seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, - minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, - hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, - days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, + days_full: { + id: 'relative_time.full.days', + defaultMessage: '{number, plural, one {# day} other {# days}} ago', + }, + moments_remaining: { + id: 'time_remaining.moments', + defaultMessage: 'Moments remaining', + }, + seconds_remaining: { + id: 'time_remaining.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} left', + }, + minutes_remaining: { + id: 'time_remaining.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} left', + }, + hours_remaining: { + id: 'time_remaining.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} left', + }, + days_remaining: { + id: 'time_remaining.days', + defaultMessage: '{number, plural, one {# day} other {# days}} left', + }, }); const dateFormatOptions = { @@ -36,8 +66,8 @@ const shortDateFormatOptions = { const SECOND = 1000; const MINUTE = 1000 * 60; -const HOUR = 1000 * 60 * 60; -const DAY = 1000 * 60 * 60 * 24; +const HOUR = 1000 * 60 * 60; +const DAY = 1000 * 60 * 60 * 24; const MAX_DELAY = 2147483647; @@ -57,20 +87,27 @@ const selectUnits = (delta: number) => { const getUnitDelay = (units: string) => { switch (units) { - case 'second': - return SECOND; - case 'minute': - return MINUTE; - case 'hour': - return HOUR; - case 'day': - return DAY; - default: - return MAX_DELAY; + case 'second': + return SECOND; + case 'minute': + return MINUTE; + case 'hour': + return HOUR; + case 'day': + return DAY; + default: + return MAX_DELAY; } }; -export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year: number, timeGiven: boolean, short?: boolean) => { +export const timeAgoString = ( + intl: InjectedIntl, + date: Date, + now: number, + year: number, + timeGiven: boolean, + short?: boolean +) => { const delta = now - date.getTime(); let relativeTime; @@ -78,27 +115,49 @@ export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year: if (delta < DAY && !timeGiven) { relativeTime = intl.formatMessage(messages.today); } else if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full); + relativeTime = intl.formatMessage( + short ? messages.just_now : messages.just_now_full + ); } else if (delta < 7 * DAY) { if (delta < MINUTE) { - relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) }); + relativeTime = intl.formatMessage( + short ? messages.seconds : messages.seconds_full, + { number: Math.floor(delta / SECOND) } + ); } else if (delta < HOUR) { - relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) }); + relativeTime = intl.formatMessage( + short ? messages.minutes : messages.minutes_full, + { number: Math.floor(delta / MINUTE) } + ); } else if (delta < DAY) { - relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) }); + relativeTime = intl.formatMessage( + short ? messages.hours : messages.hours_full, + { number: Math.floor(delta / HOUR) } + ); } else { - relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) }); + relativeTime = intl.formatMessage( + short ? messages.days : messages.days_full, + { number: Math.floor(delta / DAY) } + ); } } else if (date.getFullYear() === year) { relativeTime = intl.formatDate(date, shortDateFormatOptions); } else { - relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' }); + relativeTime = intl.formatDate(date, { + ...shortDateFormatOptions, + year: 'numeric', + }); } return relativeTime; }; -const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGiven = true) => { +const timeRemainingString = ( + intl: InjectedIntl, + date: Date, + now: number, + timeGiven = true +) => { const delta = date.getTime() - now; let relativeTime; @@ -108,13 +167,21 @@ const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGi } else if (delta < 10 * SECOND) { relativeTime = intl.formatMessage(messages.moments_remaining); } else if (delta < MINUTE) { - relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); + relativeTime = intl.formatMessage(messages.seconds_remaining, { + number: Math.floor(delta / SECOND), + }); } else if (delta < HOUR) { - relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) }); + relativeTime = intl.formatMessage(messages.minutes_remaining, { + number: Math.floor(delta / MINUTE), + }); } else if (delta < DAY) { - relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) }); + relativeTime = intl.formatMessage(messages.hours_remaining, { + number: Math.floor(delta / HOUR), + }); } else { - relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) }); + relativeTime = intl.formatMessage(messages.days_remaining, { + number: Math.floor(delta / DAY), + }); } return relativeTime; @@ -126,78 +193,88 @@ type Props = { year: number; futureDate?: boolean; short?: boolean; -} +}; type States = { now: number; -} +}; class RelativeTimestamp extends React.Component { - state = { now: this.props.intl.now(), }; static defaultProps = { - year: (new Date()).getFullYear(), + year: new Date().getFullYear(), short: true, }; _timer: number | undefined; - shouldComponentUpdate (nextProps: Props, nextState: States) { + shouldComponentUpdate(nextProps: Props, nextState: States) { // As of right now the locale doesn't change without a new page load, // but we might as well check in case that ever changes. - return this.props.timestamp !== nextProps.timestamp || + return ( + this.props.timestamp !== nextProps.timestamp || this.props.intl.locale !== nextProps.intl.locale || - this.state.now !== nextState.now; + this.state.now !== nextState.now + ); } - UNSAFE_componentWillReceiveProps (nextProps: Props) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { if (this.props.timestamp !== nextProps.timestamp) { this.setState({ now: this.props.intl.now() }); } } - componentDidMount () { + componentDidMount() { this._scheduleNextUpdate(this.props, this.state); } - UNSAFE_componentWillUpdate (nextProps: Props, nextState: States) { + UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) { this._scheduleNextUpdate(nextProps, nextState); } - componentWillUnmount () { + componentWillUnmount() { window.clearTimeout(this._timer); } - _scheduleNextUpdate (props: Props, state: States) { + _scheduleNextUpdate(props: Props, state: States) { window.clearTimeout(this._timer); - const { timestamp } = props; - const delta = (new Date(timestamp)).getTime() - state.now; - const unitDelay = getUnitDelay(selectUnits(delta)); - const unitRemainder = Math.abs(delta % unitDelay); + const { timestamp } = props; + const delta = new Date(timestamp).getTime() - state.now; + const unitDelay = getUnitDelay(selectUnits(delta)); + const unitRemainder = Math.abs(delta % unitDelay); const updateInterval = 1000 * 10; - const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder); + const delay = + delta < 0 + ? Math.max(updateInterval, unitDelay - unitRemainder) + : Math.max(updateInterval, unitRemainder); this._timer = window.setTimeout(() => { this.setState({ now: this.props.intl.now() }); }, delay); } - render () { + render() { const { timestamp, intl, year, futureDate, short } = this.props; - const timeGiven = timestamp.includes('T'); - const date = new Date(timestamp); - const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short); + const timeGiven = timestamp.includes('T'); + const date = new Date(timestamp); + const relativeTime = futureDate + ? timeRemainingString(intl, date, this.state.now, timeGiven) + : timeAgoString(intl, date, this.state.now, year, timeGiven, short); return ( -