From 51b83ed19536b06ce3f57b260400ecec2d1dd187 Mon Sep 17 00:00:00 2001
From: Nick Schonning <nschonni@gmail.com>
Date: Tue, 9 May 2023 13:02:12 -0400
Subject: [PATCH] Use Prettier for ESLint formatting TypeScript (#23631)

---
 .eslintrc.js                                  |  29 +--
 .prettierignore                               |   2 -
 .prettierrc.js                                |   3 +-
 app/javascript/mastodon/blurhash.ts           |   4 +-
 app/javascript/mastodon/compare_id.ts         |   2 +-
 .../mastodon/components/animated_number.tsx   |  50 +++--
 .../mastodon/components/avatar_overlay.tsx    |  14 +-
 .../mastodon/components/blurhash.tsx          |   2 +-
 app/javascript/mastodon/components/check.tsx  |  12 +-
 app/javascript/mastodon/components/gifv.tsx   |  26 ++-
 app/javascript/mastodon/components/icon.tsx   |  15 +-
 .../mastodon/components/icon_button.tsx       |  28 +--
 .../mastodon/components/icon_with_badge.tsx   |  15 +-
 app/javascript/mastodon/components/image.tsx  |  14 +-
 .../components/not_signed_in_indicator.tsx    |   5 +-
 .../mastodon/components/radio_button.tsx      |   8 +-
 .../components/relative_timestamp.tsx         | 189 ++++++++++++------
 app/javascript/mastodon/permissions.ts        |   6 +-
 .../mastodon/polyfills/base_polyfills.ts      |   2 +-
 .../mastodon/reducers/missed_updates.ts       |  22 +-
 app/javascript/mastodon/scroll.ts             |  37 +++-
 app/javascript/mastodon/store/index.ts        |  14 +-
 .../mastodon/store/middlewares/errors.ts      |   4 +-
 .../mastodon/store/middlewares/loading_bar.ts |  43 ++--
 .../mastodon/store/middlewares/sounds.ts      |  13 +-
 app/javascript/mastodon/utils/filters.ts      |  24 +--
 app/javascript/mastodon/utils/hashtags.ts     |  26 +--
 app/javascript/mastodon/utils/numbers.ts      |  13 +-
 app/javascript/mastodon/uuid.ts               |   4 +-
 package.json                                  |   2 +
 yarn.lock                                     |  24 +++
 31 files changed, 407 insertions(+), 245 deletions(-)

diff --git a/.eslintrc.js b/.eslintrc.js
index 2bbe301f0..9e965791b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -9,6 +9,7 @@ module.exports = {
     'plugin:import/recommended',
     'plugin:promise/recommended',
     'plugin:jsdoc/recommended',
+    'plugin:prettier/recommended',
   ],
 
   env: {
@@ -62,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',
@@ -95,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': [
@@ -107,29 +96,14 @@ module.exports = {
         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',
@@ -291,6 +265,7 @@ module.exports = {
         'plugin:import/typescript',
         'plugin:promise/recommended',
         'plugin:jsdoc/recommended',
+        'plugin:prettier/recommended',
       ],
 
       rules: {
diff --git a/.prettierignore b/.prettierignore
index 9bdf76911..2ea407533 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 1d70813d5..af39b253f 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -1,3 +1,4 @@
 module.exports = {
-  singleQuote: true
+  singleQuote: true,
+  jsxSingleQuote: true
 }
diff --git a/app/javascript/mastodon/blurhash.ts b/app/javascript/mastodon/blurhash.ts
index cb1c3b2c8..dadf2b7f2 100644
--- a/app/javascript/mastodon/blurhash.ts
+++ b/app/javascript/mastodon/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/mastodon/compare_id.ts b/app/javascript/mastodon/compare_id.ts
index 3ddfb7635..30b057248 100644
--- a/app/javascript/mastodon/compare_id.ts
+++ b/app/javascript/mastodon/compare_id.ts
@@ -1,4 +1,4 @@
-export function compareId (id1: string, id2: string) {
+export function compareId(id1: string, id2: string) {
   if (id1 === id2) {
     return 0;
   }
diff --git a/app/javascript/mastodon/components/animated_number.tsx b/app/javascript/mastodon/components/animated_number.tsx
index 20ffa1a4d..f6c77d35f 100644
--- a/app/javascript/mastodon/components/animated_number.tsx
+++ b/app/javascript/mastodon/components/animated_number.tsx
@@ -16,13 +16,10 @@ const obfuscatedCount = (count: number) => {
 type Props = {
   value: number;
   obfuscate?: boolean;
-}
-export const AnimatedNumber: React.FC<Props> = ({
-  value,
-  obfuscate,
-})=> {
+};
+export const AnimatedNumber: React.FC<Props> = ({ 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,24 +27,45 @@ export const AnimatedNumber: React.FC<Props> = ({
   }
 
   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)}</> : <ShortNumber value={value} />;
+    return obfuscate ? (
+      <>{obfuscatedCount(value)}</>
+    ) : (
+      <ShortNumber value={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 (
-    <TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
-      {items => (
+    <TransitionMotion
+      styles={styles}
+      willEnter={willEnter}
+      willLeave={willLeave}
+    >
+      {(items) => (
         <span className='animated-number'>
           {items.map(({ key, data, style }) => (
-            <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
+            <span
+              key={key}
+              style={{
+                position: direction * style.y > 0 ? 'absolute' : 'static',
+                transform: `translateY(${style.y * 100}%)`,
+              }}
+            >
+              {obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}
+            </span>
           ))}
         </span>
       )}
diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx
index e8dc88896..1dbd53323 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<Props> = ({
   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 (
     <div
-      className='account__avatar-overlay' style={{ width: size, height: size }}
+      className='account__avatar-overlay'
+      style={{ width: size, height: size }}
       onMouseEnter={handleMouseEnter}
       onMouseLeave={handleMouseLeave}
     >
diff --git a/app/javascript/mastodon/components/blurhash.tsx b/app/javascript/mastodon/components/blurhash.tsx
index 181e2183d..700513676 100644
--- a/app/javascript/mastodon/components/blurhash.tsx
+++ b/app/javascript/mastodon/components/blurhash.tsx
@@ -8,7 +8,7 @@ type Props = {
   dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
   children?: never;
   [key: string]: any;
-}
+};
 const Blurhash: React.FC<Props> = ({
   hash,
   width = 32,
diff --git a/app/javascript/mastodon/components/check.tsx b/app/javascript/mastodon/components/check.tsx
index 57b810a0e..73d65595e 100644
--- a/app/javascript/mastodon/components/check.tsx
+++ b/app/javascript/mastodon/components/check.tsx
@@ -1,7 +1,15 @@
 import React from 'react';
 
 export const Check: React.FC = () => (
-  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'>
-    <path fillRule='evenodd' d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z' clipRule='evenodd' />
+  <svg
+    xmlns='http://www.w3.org/2000/svg'
+    viewBox='0 0 20 20'
+    fill='currentColor'
+  >
+    <path
+      fillRule='evenodd'
+      d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'
+      clipRule='evenodd'
+    />
   </svg>
 );
diff --git a/app/javascript/mastodon/components/gifv.tsx b/app/javascript/mastodon/components/gifv.tsx
index 5d9f235e1..72914ba74 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<Props> = ({
   src,
@@ -17,19 +17,23 @@ export const GIFV: React.FC<Props> = ({
   width,
   height,
   onClick,
-})=> {
+}) => {
   const [loading, setLoading] = useState(true);
 
-  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => {
-    setLoading(false);
-  }, [setLoading]);
+  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
+    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 (
     <div className='gifv' style={{ position: 'relative' }}>
diff --git a/app/javascript/mastodon/components/icon.tsx b/app/javascript/mastodon/components/icon.tsx
index f74437b55..4eb948dc7 100644
--- a/app/javascript/mastodon/components/icon.tsx
+++ b/app/javascript/mastodon/components/icon.tsx
@@ -7,6 +7,15 @@ type Props = {
   fixedWidth?: boolean;
   children?: never;
   [key: string]: any;
-}
-export const Icon: React.FC<Props> = ({ id, className, fixedWidth, ...other }) =>
-  <i className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />;
+};
+export const Icon: React.FC<Props> = ({
+  id,
+  className,
+  fixedWidth,
+  ...other
+}) => (
+  <i
+    className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
+    {...other}
+  />
+);
diff --git a/app/javascript/mastodon/components/icon_button.tsx b/app/javascript/mastodon/components/icon_button.tsx
index f9db287bb..178641400 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,
-}
+  activate: boolean;
+  deactivate: boolean;
+};
 export class IconButton extends React.PureComponent<Props, States> {
-
   static defaultProps = {
     size: 18,
     active: false,
@@ -47,7 +46,7 @@ export class IconButton extends React.PureComponent<Props, States> {
     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 class IconButton extends React.PureComponent<Props, States> {
     }
   }
 
-  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) =>  {
+  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
     e.preventDefault();
 
     if (!this.props.disabled && this.props.onClick != null) {
@@ -83,7 +82,7 @@ export class IconButton extends React.PureComponent<Props, States> {
     }
   };
 
-  render () {
+  render() {
     const style = {
       fontSize: `${this.props.size}px`,
       width: `${this.props.size * 1.28571429}px`,
@@ -109,10 +108,7 @@ export class IconButton extends React.PureComponent<Props, States> {
       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 class IconButton extends React.PureComponent<Props, States> {
 
     let contents = (
       <React.Fragment>
-        <Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
+        <Icon id={icon} fixedWidth aria-hidden='true' />{' '}
+        {typeof counter !== 'undefined' && (
+          <span className='icon-button__counter'>
+            <AnimatedNumber value={counter} obfuscate={obfuscateCount} />
+          </span>
+        )}
       </React.Fragment>
     );
 
@@ -162,5 +163,4 @@ export class IconButton extends React.PureComponent<Props, States> {
       </button>
     );
   }
-
 }
diff --git a/app/javascript/mastodon/components/icon_with_badge.tsx b/app/javascript/mastodon/components/icon_with_badge.tsx
index a4af86ca9..bf86814c0 100644
--- a/app/javascript/mastodon/components/icon_with_badge.tsx
+++ b/app/javascript/mastodon/components/icon_with_badge.tsx
@@ -1,18 +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;
-}
-export const IconWithBadge: React.FC<Props> = ({ id, count, issueBadge, className }) => (
+};
+export const IconWithBadge: React.FC<Props> = ({
+  id,
+  count,
+  issueBadge,
+  className,
+}) => (
   <i className='icon-with-badge'>
     <Icon id={id} fixedWidth className={className} />
-    {count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
+    {count > 0 && (
+      <i className='icon-with-badge__badge'>{formatNumber(count)}</i>
+    )}
     {issueBadge && <i className='icon-with-badge__issue-badge' />}
   </i>
 );
diff --git a/app/javascript/mastodon/components/image.tsx b/app/javascript/mastodon/components/image.tsx
index b332d4115..490543424 100644
--- a/app/javascript/mastodon/components/image.tsx
+++ b/app/javascript/mastodon/components/image.tsx
@@ -7,9 +7,14 @@ type Props = {
   srcSet?: string;
   blurhash?: string;
   className?: string;
-}
+};
 
-export const Image: React.FC<Props> = ({ src, srcSet, blurhash, className }) => {
+export const Image: React.FC<Props> = ({
+  src,
+  srcSet,
+  blurhash,
+  className,
+}) => {
   const [loaded, setLoaded] = useState(false);
 
   const handleLoad = useCallback(() => {
@@ -17,7 +22,10 @@ export const Image: React.FC<Props> = ({ src, srcSet, blurhash, className }) =>
   }, [setLoaded]);
 
   return (
-    <div className={classNames('image', { loaded }, className)} role='presentation'>
+    <div
+      className={classNames('image', { loaded }, className)}
+      role='presentation'
+    >
       {blurhash && <Blurhash hash={blurhash} className='image__preview' />}
       <img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
     </div>
diff --git a/app/javascript/mastodon/components/not_signed_in_indicator.tsx b/app/javascript/mastodon/components/not_signed_in_indicator.tsx
index 3bfee6ae9..53945d6a7 100644
--- a/app/javascript/mastodon/components/not_signed_in_indicator.tsx
+++ b/app/javascript/mastodon/components/not_signed_in_indicator.tsx
@@ -4,7 +4,10 @@ import { FormattedMessage } from 'react-intl';
 export const NotSignedInIndicator: React.FC = () => (
   <div className='scrollable scrollable--flex'>
     <div className='empty-column-indicator'>
-      <FormattedMessage id='not_signed_in_indicator.not_signed_in' defaultMessage='You need to sign in to access this resource.' />
+      <FormattedMessage
+        id='not_signed_in_indicator.not_signed_in'
+        defaultMessage='You need to sign in to access this resource.'
+      />
     </div>
   </div>
 );
diff --git a/app/javascript/mastodon/components/radio_button.tsx b/app/javascript/mastodon/components/radio_button.tsx
index 194b67afe..829f47174 100644
--- a/app/javascript/mastodon/components/radio_button.tsx
+++ b/app/javascript/mastodon/components/radio_button.tsx
@@ -9,7 +9,13 @@ type Props = {
   label: React.ReactNode;
 };
 
-export const RadioButton: React.FC<Props> = ({ name, value, checked, onChange, label }) => {
+export const RadioButton: React.FC<Props> = ({
+  name,
+  value,
+  checked,
+  onChange,
+  label,
+}) => {
   return (
     <label className='radio-button'>
       <input
diff --git a/app/javascript/mastodon/components/relative_timestamp.tsx b/app/javascript/mastodon/components/relative_timestamp.tsx
index 4f1a76e54..65d9d27cb 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,86 @@ type Props = {
   year: number;
   futureDate?: boolean;
   short?: boolean;
-}
+};
 type States = {
   now: number;
-}
+};
 class RelativeTimestamp extends React.Component<Props, States> {
-
   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 (
-      <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
+      <time
+        dateTime={timestamp}
+        title={intl.formatDate(date, dateFormatOptions)}
+      >
         {relativeTime}
       </time>
     );
   }
-
 }
 
 const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp);
diff --git a/app/javascript/mastodon/permissions.ts b/app/javascript/mastodon/permissions.ts
index 9ea149e5f..b583535c0 100644
--- a/app/javascript/mastodon/permissions.ts
+++ b/app/javascript/mastodon/permissions.ts
@@ -1,4 +1,4 @@
-export const PERMISSION_INVITE_USERS      = 0x0000000000010000;
-export const PERMISSION_MANAGE_USERS      = 0x0000000000000400;
+export const PERMISSION_INVITE_USERS = 0x0000000000010000;
+export const PERMISSION_MANAGE_USERS = 0x0000000000000400;
 export const PERMISSION_MANAGE_FEDERATION = 0x0000000000000020;
-export const PERMISSION_MANAGE_REPORTS    = 0x0000000000000010;
+export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010;
diff --git a/app/javascript/mastodon/polyfills/base_polyfills.ts b/app/javascript/mastodon/polyfills/base_polyfills.ts
index 2e583f580..64211c11e 100644
--- a/app/javascript/mastodon/polyfills/base_polyfills.ts
+++ b/app/javascript/mastodon/polyfills/base_polyfills.ts
@@ -10,7 +10,7 @@ if (!HTMLCanvasElement.prototype.toBlob) {
   const BASE64_MARKER = ';base64,';
 
   Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
-    value(callback: BlobCallback, type = 'image/png', quality: any)  {
+    value(callback: BlobCallback, type = 'image/png', quality: any) {
       const dataURL = this.toDataURL(type, quality);
       let data;
 
diff --git a/app/javascript/mastodon/reducers/missed_updates.ts b/app/javascript/mastodon/reducers/missed_updates.ts
index 628fed2a9..9c1a5cbd2 100644
--- a/app/javascript/mastodon/reducers/missed_updates.ts
+++ b/app/javascript/mastodon/reducers/missed_updates.ts
@@ -14,18 +14,18 @@ const initialState = Record<MissedUpdatesState>({
 
 export function missedUpdatesReducer(
   state = initialState,
-  action: Action<string>,
+  action: Action<string>
 ) {
   switch (action.type) {
-  case focusApp.type:
-    return state.set('focused', true).set('unread', 0);
-  case unfocusApp.type:
-    return state.set('focused', false);
-  case NOTIFICATIONS_UPDATE:
-    return state.get('focused')
-      ? state
-      : state.update('unread', (x) => x + 1);
-  default:
-    return state;
+    case focusApp.type:
+      return state.set('focused', true).set('unread', 0);
+    case unfocusApp.type:
+      return state.set('focused', false);
+    case NOTIFICATIONS_UPDATE:
+      return state.get('focused')
+        ? state
+        : state.update('unread', (x) => x + 1);
+    default:
+      return state;
   }
 }
diff --git a/app/javascript/mastodon/scroll.ts b/app/javascript/mastodon/scroll.ts
index 1e509c417..625aab0c0 100644
--- a/app/javascript/mastodon/scroll.ts
+++ b/app/javascript/mastodon/scroll.ts
@@ -1,13 +1,23 @@
-const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
-const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => {
+const easingOutQuint = (
+  x: number,
+  t: number,
+  b: number,
+  c: number,
+  d: number
+) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
+const scroll = (
+  node: Element,
+  key: 'scrollTop' | 'scrollLeft',
+  target: number
+) => {
   const startTime = Date.now();
-  const offset    = node[key];
-  const gap       = target - offset;
-  const duration  = 1000;
-  let interrupt   = false;
+  const offset = node[key];
+  const gap = target - offset;
+  const duration = 1000;
+  let interrupt = false;
 
   const step = () => {
-    const elapsed    = Date.now() - startTime;
+    const elapsed = Date.now() - startTime;
     const percentage = elapsed / duration;
 
     if (percentage > 1 || interrupt) {
@@ -25,7 +35,14 @@ const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number)
   };
 };
 
-const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style;
+const isScrollBehaviorSupported =
+  'scrollBehavior' in document.documentElement.style;
 
-export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
-export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
+export const scrollRight = (node: Element, position: number) =>
+  isScrollBehaviorSupported
+    ? node.scrollTo({ left: position, behavior: 'smooth' })
+    : scroll(node, 'scrollLeft', position);
+export const scrollTop = (node: Element) =>
+  isScrollBehaviorSupported
+    ? node.scrollTo({ top: 0, behavior: 'smooth' })
+    : scroll(node, 'scrollTop', 0);
diff --git a/app/javascript/mastodon/store/index.ts b/app/javascript/mastodon/store/index.ts
index 822c01aa9..6c3e963d9 100644
--- a/app/javascript/mastodon/store/index.ts
+++ b/app/javascript/mastodon/store/index.ts
@@ -7,17 +7,21 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
 
 export const store = configureStore({
   reducer: rootReducer,
-  middleware: getDefaultMiddleware =>
-    getDefaultMiddleware().concat(
-      loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }))
+  middleware: (getDefaultMiddleware) =>
+    getDefaultMiddleware()
+      .concat(
+        loadingBarMiddleware({
+          promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
+        })
+      )
       .concat(errorsMiddleware)
       .concat(soundsMiddleware()),
 });
 
 // Infer the `RootState` and `AppDispatch` types from the store itself
-export type RootState = ReturnType<typeof rootReducer>
+export type RootState = ReturnType<typeof rootReducer>;
 // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
-export type AppDispatch = typeof store.dispatch
+export type AppDispatch = typeof store.dispatch;
 
 export const useAppDispatch: () => AppDispatch = useDispatch;
 export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
diff --git a/app/javascript/mastodon/store/middlewares/errors.ts b/app/javascript/mastodon/store/middlewares/errors.ts
index b135fa2ee..a5e99d04e 100644
--- a/app/javascript/mastodon/store/middlewares/errors.ts
+++ b/app/javascript/mastodon/store/middlewares/errors.ts
@@ -5,7 +5,9 @@ import { RootState } from '..';
 const defaultFailSuffix = 'FAIL';
 
 export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
-  ({ dispatch }) => next => action => {
+  ({ dispatch }) =>
+  (next) =>
+  (action) => {
     if (action.type && !action.skipAlert) {
       const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
 
diff --git a/app/javascript/mastodon/store/middlewares/loading_bar.ts b/app/javascript/mastodon/store/middlewares/loading_bar.ts
index e860b31b6..183c0cf9d 100644
--- a/app/javascript/mastodon/store/middlewares/loading_bar.ts
+++ b/app/javascript/mastodon/store/middlewares/loading_bar.ts
@@ -3,29 +3,40 @@ import { Middleware } from 'redux';
 import { RootState } from '..';
 
 interface Config {
-  promiseTypeSuffixes?: string[]
+  promiseTypeSuffixes?: string[];
 }
 
-const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = ['PENDING', 'FULFILLED', 'REJECTED'];
+const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
+  'PENDING',
+  'FULFILLED',
+  'REJECTED',
+];
 
-export  const loadingBarMiddleware = (config: Config = {}): Middleware<Record<string, never>, RootState> => {
+export const loadingBarMiddleware = (
+  config: Config = {}
+): Middleware<Record<string, never>, RootState> => {
   const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes;
 
-  return ({ dispatch }) => next => (action) => {
-    if (action.type && !action.skipLoading) {
-      const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
+  return ({ dispatch }) =>
+    (next) =>
+    (action) => {
+      if (action.type && !action.skipLoading) {
+        const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
 
-      const isPending = new RegExp(`${PENDING}$`, 'g');
-      const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
-      const isRejected = new RegExp(`${REJECTED}$`, 'g');
+        const isPending = new RegExp(`${PENDING}$`, 'g');
+        const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
+        const isRejected = new RegExp(`${REJECTED}$`, 'g');
 
-      if (action.type.match(isPending)) {
-        dispatch(showLoading());
-      } else if (action.type.match(isFulfilled) || action.type.match(isRejected)) {
-        dispatch(hideLoading());
+        if (action.type.match(isPending)) {
+          dispatch(showLoading());
+        } else if (
+          action.type.match(isFulfilled) ||
+          action.type.match(isRejected)
+        ) {
+          dispatch(hideLoading());
+        }
       }
-    }
 
-    return next(action);
-  };
+      return next(action);
+    };
 };
diff --git a/app/javascript/mastodon/store/middlewares/sounds.ts b/app/javascript/mastodon/store/middlewares/sounds.ts
index c9d51f857..e7c87df7e 100644
--- a/app/javascript/mastodon/store/middlewares/sounds.ts
+++ b/app/javascript/mastodon/store/middlewares/sounds.ts
@@ -2,8 +2,8 @@ import { Middleware, AnyAction } from 'redux';
 import { RootState } from '..';
 
 interface AudioSource {
-  src: string
-  type: string
+  src: string;
+  type: string;
 }
 
 const createAudio = (sources: AudioSource[]) => {
@@ -30,8 +30,11 @@ const play = (audio: HTMLAudioElement) => {
   audio.play();
 };
 
-export  const soundsMiddleware = (): Middleware<Record<string, never>, RootState> => {
-  const soundCache: {[key: string]: HTMLAudioElement} = {
+export const soundsMiddleware = (): Middleware<
+  Record<string, never>,
+  RootState
+> => {
+  const soundCache: { [key: string]: HTMLAudioElement } = {
     boop: createAudio([
       {
         src: '/sounds/boop.ogg',
@@ -44,7 +47,7 @@ export  const soundsMiddleware = (): Middleware<Record<string, never>, RootState
     ]),
   };
 
-  return () => next => (action: AnyAction) => {
+  return () => (next) => (action: AnyAction) => {
     const sound = action?.meta?.sound;
 
     if (sound && soundCache[sound]) {
diff --git a/app/javascript/mastodon/utils/filters.ts b/app/javascript/mastodon/utils/filters.ts
index 5af2aa96a..e5c6422e0 100644
--- a/app/javascript/mastodon/utils/filters.ts
+++ b/app/javascript/mastodon/utils/filters.ts
@@ -1,16 +1,16 @@
 export const toServerSideType = (columnType: string) => {
   switch (columnType) {
-  case 'home':
-  case 'notifications':
-  case 'public':
-  case 'thread':
-  case 'account':
-    return columnType;
-  default:
-    if (columnType.indexOf('list:') > -1) {
-      return 'home';
-    } else {
-      return 'public'; // community, account, hashtag
-    }
+    case 'home':
+    case 'notifications':
+    case 'public':
+    case 'thread':
+    case 'account':
+      return columnType;
+    default:
+      if (columnType.indexOf('list:') > -1) {
+        return 'home';
+      } else {
+        return 'public'; // community, account, hashtag
+      }
   }
 };
diff --git a/app/javascript/mastodon/utils/hashtags.ts b/app/javascript/mastodon/utils/hashtags.ts
index 358ce37f5..4c76cd7de 100644
--- a/app/javascript/mastodon/utils/hashtags.ts
+++ b/app/javascript/mastodon/utils/hashtags.ts
@@ -5,17 +5,8 @@ const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
 const buildHashtagPatternRegex = () => {
   try {
     return new RegExp(
-      '(?:^|[^\\/\\)\\w])#((' +
-      '[' + WORD + '_]' +
-      '[' + WORD + HASHTAG_SEPARATORS + ']*' +
-      '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
-      '[' + WORD + HASHTAG_SEPARATORS +']*' +
-      '[' + WORD + '_]' +
-      ')|(' +
-      '[' + WORD + '_]*' +
-      '[' + ALPHA + ']' +
-      '[' + WORD + '_]*' +
-      '))', 'iu',
+      `(?:^|[^\\/\\)\\w])#(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))`,
+      'iu'
     );
   } catch {
     return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
@@ -25,17 +16,8 @@ const buildHashtagPatternRegex = () => {
 const buildHashtagRegex = () => {
   try {
     return new RegExp(
-      '^((' +
-      '[' + WORD + '_]' +
-      '[' + WORD + HASHTAG_SEPARATORS + ']*' +
-      '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
-      '[' + WORD + HASHTAG_SEPARATORS +']*' +
-      '[' + WORD + '_]' +
-      ')|(' +
-      '[' + WORD + '_]*' +
-      '[' + ALPHA + ']' +
-      '[' + WORD + '_]*' +
-      '))$', 'iu',
+      `^(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))$`,
+      'iu'
     );
   } catch {
     return /^(\w*[a-zA-Z·]\w*)$/i;
diff --git a/app/javascript/mastodon/utils/numbers.ts b/app/javascript/mastodon/utils/numbers.ts
index 35af8a973..a4a028c30 100644
--- a/app/javascript/mastodon/utils/numbers.ts
+++ b/app/javascript/mastodon/utils/numbers.ts
@@ -21,7 +21,7 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
  * shortNumber(5936);
  * // => [5.936, 1000, 1]
  */
-export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits
+export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten number, unit of shorten number and maximum fraction digits
 export function toShortNumber(sourceNumber: number): ShortNumber {
   if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
     return [sourceNumber, DECIMAL_UNITS.ONE, 0];
@@ -38,11 +38,7 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
       sourceNumber < TEN_MILLIONS ? 1 : 0,
     ];
   } else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
-    return [
-      sourceNumber / DECIMAL_UNITS.BILLION,
-      DECIMAL_UNITS.BILLION,
-      0,
-    ];
+    return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0];
   }
 
   return [sourceNumber, DECIMAL_UNITS.ONE, 0];
@@ -56,7 +52,10 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
  * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
  * // => 1790
  */
-export function pluralReady(sourceNumber: number, division: DecimalUnits): number {
+export function pluralReady(
+  sourceNumber: number,
+  division: DecimalUnits
+): number {
   if (division == null || division < DECIMAL_UNITS.HUNDRED) {
     return sourceNumber;
   }
diff --git a/app/javascript/mastodon/uuid.ts b/app/javascript/mastodon/uuid.ts
index cabbc0f4e..6cadbd6bb 100644
--- a/app/javascript/mastodon/uuid.ts
+++ b/app/javascript/mastodon/uuid.ts
@@ -1,8 +1,8 @@
 export function uuid(a?: string): string {
   return a
     ? (
-      (a as any as number) ^
+        (a as any as number) ^
         ((Math.random() * 16) >> ((a as any as number) / 4))
-    ).toString(16)
+      ).toString(16)
     : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
 }
diff --git a/package.json b/package.json
index 9129c8b0a..b326dfd45 100644
--- a/package.json
+++ b/package.json
@@ -180,10 +180,12 @@
     "@typescript-eslint/parser": "^5.59.5",
     "babel-jest": "^29.5.0",
     "eslint": "^8.39.0",
+    "eslint-config-prettier": "^8.8.0",
     "eslint-plugin-formatjs": "^4.10.1",
     "eslint-plugin-import": "~2.27.5",
     "eslint-plugin-jsdoc": "^43.1.1",
     "eslint-plugin-jsx-a11y": "~6.7.1",
+    "eslint-plugin-prettier": "^4.2.1",
     "eslint-plugin-promise": "~6.1.1",
     "eslint-plugin-react": "~7.32.2",
     "eslint-plugin-react-hooks": "^4.6.0",
diff --git a/yarn.lock b/yarn.lock
index 72fb0a333..cd0abca76 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5001,6 +5001,11 @@ escodegen@^2.0.0:
   optionalDependencies:
     source-map "~0.6.1"
 
+eslint-config-prettier@^8.8.0:
+  version "8.8.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
+  integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
+
 eslint-import-resolver-node@^0.3.7:
   version "0.3.7"
   resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7"
@@ -5091,6 +5096,13 @@ eslint-plugin-jsx-a11y@~6.7.1:
     object.fromentries "^2.0.6"
     semver "^6.3.0"
 
+eslint-plugin-prettier@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
+  integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
+  dependencies:
+    prettier-linter-helpers "^1.0.0"
+
 eslint-plugin-promise@~6.1.1:
   version "6.1.1"
   resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816"
@@ -5430,6 +5442,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
+fast-diff@^1.1.2:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
+  integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
+
 fast-glob@^3.2.12, fast-glob@^3.2.9:
   version "3.2.12"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
@@ -9199,6 +9216,13 @@ prelude-ls@~1.1.2:
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
   integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
 
+prettier-linter-helpers@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
+  integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
+  dependencies:
+    fast-diff "^1.1.2"
+
 prettier@^2.8.8:
   version "2.8.8"
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"