diff --git a/.eslintrc.js b/.eslintrc.js index a9aa86605926f718519849d8b170b5cb8165bf1b..176879034bea4f5da93a313c8c348070bceb3fb1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -284,7 +284,6 @@ module.exports = defineConfig({ 'formatjs/no-id': 'off', // IDs are used for translation keys 'formatjs/no-invalid-icu': 'error', 'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings - 'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx 'formatjs/no-multiple-whitespaces': 'error', 'formatjs/no-offset': 'error', 'formatjs/no-useless-message': 'error', @@ -354,7 +353,7 @@ module.exports = defineConfig({ '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], '@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/consistent-type-imports': 'error', - "@typescript-eslint/prefer-nullish-coalescing": ['error', {ignorePrimitives: {boolean: true}}], + "@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }], 'jsdoc/require-jsdoc': 'off', diff --git a/app/javascript/mastodon/components/admin/Trends.jsx b/app/javascript/mastodon/components/admin/Trends.jsx index 49976276ee50117619319872ed10f4c7876e3bae..c69b4a8cbaf4149cb3a11c951e71538fad76b552 100644 --- a/app/javascript/mastodon/components/admin/Trends.jsx +++ b/app/javascript/mastodon/components/admin/Trends.jsx @@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import api from 'mastodon/api'; -import Hashtag from 'mastodon/components/hashtag'; +import { Hashtag } from 'mastodon/components/hashtag'; export default class Trends extends PureComponent { diff --git a/app/javascript/mastodon/components/hashtag.jsx b/app/javascript/mastodon/components/hashtag.jsx deleted file mode 100644 index 14bb4ddc64c8f12756cf136ebef7eaa693bba18c..0000000000000000000000000000000000000000 --- a/app/javascript/mastodon/components/hashtag.jsx +++ /dev/null @@ -1,120 +0,0 @@ -// @ts-check -import PropTypes from 'prop-types'; -import { Component } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; -import { Link } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; - -import { Sparklines, SparklinesCurve } from 'react-sparklines'; - -import { ShortNumber } from 'mastodon/components/short_number'; -import { Skeleton } from 'mastodon/components/skeleton'; - -class SilentErrorBoundary extends Component { - - static propTypes = { - children: PropTypes.node, - }; - - state = { - error: false, - }; - - componentDidCatch() { - this.setState({ error: true }); - } - - render() { - if (this.state.error) { - return null; - } - - return this.props.children; - } - -} - -/** - * Used to render counter of how much people are talking about hashtag - * @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} - */ -export const accountsCountRenderer = (displayNumber, pluralReady) => ( - <FormattedMessage - id='trends.counter_by_accounts' - defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}' - values={{ - count: pluralReady, - counter: <strong>{displayNumber}</strong>, - days: 2, - }} - /> -); - -// @ts-expect-error -export const ImmutableHashtag = ({ hashtag }) => ( - <Hashtag - name={hashtag.get('name')} - to={`/tags/${hashtag.get('name')}`} - people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1} - // @ts-expect-error - history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()} - /> -); - -ImmutableHashtag.propTypes = { - hashtag: ImmutablePropTypes.map.isRequired, -}; - -// @ts-expect-error -const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => ( - <div className={classNames('trends__item', className)}> - <div className='trends__item__name'> - <Link to={to}> - {name ? <>#<span>{name}</span></> : <Skeleton width={50} />} - </Link> - - {description ? ( - <span>{description}</span> - ) : ( - typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} /> - )} - </div> - - {typeof uses !== 'undefined' && ( - <div className='trends__item__current'> - <ShortNumber value={uses} /> - </div> - )} - - {withGraph && ( - <div className='trends__item__sparkline'> - <SilentErrorBoundary> - <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}> - <SparklinesCurve style={{ fill: 'none' }} /> - </Sparklines> - </SilentErrorBoundary> - </div> - )} - </div> -); - -Hashtag.propTypes = { - name: PropTypes.string, - to: PropTypes.string, - people: PropTypes.number, - description: PropTypes.node, - uses: PropTypes.number, - history: PropTypes.arrayOf(PropTypes.number), - className: PropTypes.string, - withGraph: PropTypes.bool, -}; - -Hashtag.defaultProps = { - withGraph: true, -}; - -export default Hashtag; diff --git a/app/javascript/mastodon/components/hashtag.tsx b/app/javascript/mastodon/components/hashtag.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8963e4a40d820dd40b9c7eb07c6070cf47cb8a84 --- /dev/null +++ b/app/javascript/mastodon/components/hashtag.tsx @@ -0,0 +1,145 @@ +import type { JSX } from 'react'; +import { Component } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; +import { Link } from 'react-router-dom'; + +import type Immutable from 'immutable'; + +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +import { ShortNumber } from 'mastodon/components/short_number'; +import { Skeleton } from 'mastodon/components/skeleton'; + +interface SilentErrorBoundaryProps { + children: React.ReactNode; +} + +class SilentErrorBoundary extends Component<SilentErrorBoundaryProps> { + state = { + error: false, + }; + + componentDidCatch() { + this.setState({ error: true }); + } + + render() { + if (this.state.error) { + return null; + } + + return this.props.children; + } +} + +/** + * Used to render counter of how much people are talking about hashtag + * @param displayNumber Counter number to display + * @param pluralReady Whether the count is plural + * @returns Formatted counter of how much people are talking about hashtag + */ +export const accountsCountRenderer = ( + displayNumber: JSX.Element, + pluralReady: number, +) => ( + <FormattedMessage + id='trends.counter_by_accounts' + defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}' + values={{ + count: pluralReady, + counter: <strong>{displayNumber}</strong>, + days: 2, + }} + /> +); + +interface ImmutableHashtagProps { + hashtag: Immutable.Map<string, unknown>; +} + +export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => ( + <Hashtag + name={hashtag.get('name') as string} + to={`/tags/${hashtag.get('name') as string}`} + people={ + (hashtag.getIn(['history', 0, 'accounts']) as number) * 1 + + (hashtag.getIn(['history', 1, 'accounts']) as number) * 1 + } + history={( + hashtag.get('history') as Immutable.Collection.Indexed< + Immutable.Map<string, number> + > + ) + .reverse() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .map((day) => day.get('uses')!) + .toArray()} + /> +); + +export interface HashtagProps { + className?: string; + description?: React.ReactNode; + history?: number[]; + name: string; + people: number; + to: string; + uses?: number; + withGraph?: boolean; +} + +export const Hashtag: React.FC<HashtagProps> = ({ + name, + to, + people, + uses, + history, + className, + description, + withGraph = true, +}) => ( + <div className={classNames('trends__item', className)}> + <div className='trends__item__name'> + <Link to={to}> + {name ? ( + <> + #<span>{name}</span> + </> + ) : ( + <Skeleton width={50} /> + )} + </Link> + + {description ? ( + <span>{description}</span> + ) : typeof people !== 'undefined' ? ( + <ShortNumber value={people} renderer={accountsCountRenderer} /> + ) : ( + <Skeleton width={100} /> + )} + </div> + + {typeof uses !== 'undefined' && ( + <div className='trends__item__current'> + <ShortNumber value={uses} /> + </div> + )} + + {withGraph && ( + <div className='trends__item__sparkline'> + <SilentErrorBoundary> + <Sparklines + width={50} + height={28} + data={history ? history : Array.from(Array(7)).map(() => 0)} + > + <SparklinesCurve style={{ fill: 'none' }} /> + </Sparklines> + </SilentErrorBoundary> + </div> + )} + </div> +); diff --git a/app/javascript/mastodon/features/account/components/featured_tags.jsx b/app/javascript/mastodon/features/account/components/featured_tags.jsx index 4d7dd86560a34de8107d5c1b49125fc3bc89e7f9..56a9efac0227095e7b031fc7c69fa5b9df5038f1 100644 --- a/app/javascript/mastodon/features/account/components/featured_tags.jsx +++ b/app/javascript/mastodon/features/account/components/featured_tags.jsx @@ -5,7 +5,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Hashtag from 'mastodon/components/hashtag'; +import { Hashtag } from 'mastodon/components/hashtag'; const messages = defineMessages({ lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' }, diff --git a/app/javascript/mastodon/features/followed_tags/index.jsx b/app/javascript/mastodon/features/followed_tags/index.jsx index 7042f2438aa2cc9d17d9b6f3fb97220b02f8d6e5..dec53f012101075da4b7133f3d4ea8e96b4ed16e 100644 --- a/app/javascript/mastodon/features/followed_tags/index.jsx +++ b/app/javascript/mastodon/features/followed_tags/index.jsx @@ -13,7 +13,7 @@ import { debounce } from 'lodash'; import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags'; import ColumnHeader from 'mastodon/components/column_header'; -import Hashtag from 'mastodon/components/hashtag'; +import { Hashtag } from 'mastodon/components/hashtag'; import ScrollableList from 'mastodon/components/scrollable_list'; import Column from 'mastodon/features/ui/components/column';