Use Prettier for ESLint formatting TypeScript (#23631)
This commit is contained in:
		
							
								
								
									
										29
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								.eslintrc.js
									
									
									
									
									
								
							@@ -9,6 +9,7 @@ module.exports = {
 | 
				
			|||||||
    'plugin:import/recommended',
 | 
					    'plugin:import/recommended',
 | 
				
			||||||
    'plugin:promise/recommended',
 | 
					    'plugin:promise/recommended',
 | 
				
			||||||
    'plugin:jsdoc/recommended',
 | 
					    'plugin:jsdoc/recommended',
 | 
				
			||||||
 | 
					    'plugin:prettier/recommended',
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  env: {
 | 
					  env: {
 | 
				
			||||||
@@ -62,20 +63,9 @@ module.exports = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  rules: {
 | 
					  rules: {
 | 
				
			||||||
    'brace-style': 'warn',
 | 
					 | 
				
			||||||
    'comma-dangle': ['error', 'always-multiline'],
 | 
					 | 
				
			||||||
    'comma-spacing': [
 | 
					 | 
				
			||||||
      'warn',
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        before: false,
 | 
					 | 
				
			||||||
        after: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    'comma-style': ['warn', 'last'],
 | 
					 | 
				
			||||||
    'consistent-return': 'error',
 | 
					    'consistent-return': 'error',
 | 
				
			||||||
    'dot-notation': 'error',
 | 
					    'dot-notation': 'error',
 | 
				
			||||||
    eqeqeq: ['error', 'always', { 'null': 'ignore' }],
 | 
					    eqeqeq: ['error', 'always', { 'null': 'ignore' }],
 | 
				
			||||||
    indent: ['warn', 2],
 | 
					 | 
				
			||||||
    'jsx-quotes': ['error', 'prefer-single'],
 | 
					    'jsx-quotes': ['error', 'prefer-single'],
 | 
				
			||||||
    'no-case-declarations': 'off',
 | 
					    'no-case-declarations': 'off',
 | 
				
			||||||
    'no-catch-shadow': 'error',
 | 
					    'no-catch-shadow': 'error',
 | 
				
			||||||
@@ -95,7 +85,6 @@ module.exports = {
 | 
				
			|||||||
      { property: 'substr', message: 'Use .slice instead of .substr.' },
 | 
					      { property: 'substr', message: 'Use .slice instead of .substr.' },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    'no-self-assign': 'off',
 | 
					    'no-self-assign': 'off',
 | 
				
			||||||
    'no-trailing-spaces': 'warn',
 | 
					 | 
				
			||||||
    'no-unused-expressions': 'error',
 | 
					    'no-unused-expressions': 'error',
 | 
				
			||||||
    'no-unused-vars': 'off',
 | 
					    'no-unused-vars': 'off',
 | 
				
			||||||
    '@typescript-eslint/no-unused-vars': [
 | 
					    '@typescript-eslint/no-unused-vars': [
 | 
				
			||||||
@@ -107,29 +96,14 @@ module.exports = {
 | 
				
			|||||||
        ignoreRestSiblings: true,
 | 
					        ignoreRestSiblings: true,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    'object-curly-spacing': ['error', 'always'],
 | 
					 | 
				
			||||||
    'padded-blocks': [
 | 
					 | 
				
			||||||
      'error',
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        classes: 'always',
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    quotes: ['error', 'single'],
 | 
					 | 
				
			||||||
    semi: 'error',
 | 
					 | 
				
			||||||
    'valid-typeof': 'error',
 | 
					    'valid-typeof': 'error',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }],
 | 
					    'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }],
 | 
				
			||||||
    'react/jsx-boolean-value': 'error',
 | 
					    'react/jsx-boolean-value': 'error',
 | 
				
			||||||
    'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
 | 
					 | 
				
			||||||
    'react/jsx-curly-spacing': 'error',
 | 
					 | 
				
			||||||
    'react/display-name': 'off',
 | 
					    'react/display-name': 'off',
 | 
				
			||||||
    'react/jsx-equals-spacing': 'error',
 | 
					    '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-bind': 'error',
 | 
				
			||||||
    'react/jsx-no-target-blank': 'off',
 | 
					    'react/jsx-no-target-blank': 'off',
 | 
				
			||||||
    'react/jsx-tag-spacing': 'error',
 | 
					 | 
				
			||||||
    'react/jsx-wrap-multilines': 'error',
 | 
					 | 
				
			||||||
    'react/no-deprecated': 'off',
 | 
					    'react/no-deprecated': 'off',
 | 
				
			||||||
    'react/no-unknown-property': 'off',
 | 
					    'react/no-unknown-property': 'off',
 | 
				
			||||||
    'react/self-closing-comp': 'error',
 | 
					    'react/self-closing-comp': 'error',
 | 
				
			||||||
@@ -291,6 +265,7 @@ module.exports = {
 | 
				
			|||||||
        'plugin:import/typescript',
 | 
					        'plugin:import/typescript',
 | 
				
			||||||
        'plugin:promise/recommended',
 | 
					        'plugin:promise/recommended',
 | 
				
			||||||
        'plugin:jsdoc/recommended',
 | 
					        'plugin:jsdoc/recommended',
 | 
				
			||||||
 | 
					        'plugin:prettier/recommended',
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      rules: {
 | 
					      rules: {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,8 +70,6 @@ app/javascript/styles/mastodon/reset.scss
 | 
				
			|||||||
# Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631
 | 
					# Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631
 | 
				
			||||||
*.js
 | 
					*.js
 | 
				
			||||||
*.jsx
 | 
					*.jsx
 | 
				
			||||||
*.ts
 | 
					 | 
				
			||||||
*.tsx
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Ignore HTML till cleaned and included in CI
 | 
					# Ignore HTML till cleaned and included in CI
 | 
				
			||||||
*.html
 | 
					*.html
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  singleQuote: true
 | 
					  singleQuote: true,
 | 
				
			||||||
 | 
					  jsxSingleQuote: true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -98,9 +98,9 @@ export const decode83 = (str: string) => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const intToRGB = (int: number) => ({
 | 
					export const intToRGB = (int: number) => ({
 | 
				
			||||||
  r: Math.max(0, (int >> 16)),
 | 
					  r: Math.max(0, int >> 16),
 | 
				
			||||||
  g: Math.max(0, (int >> 8) & 255),
 | 
					  g: Math.max(0, (int >> 8) & 255),
 | 
				
			||||||
  b: Math.max(0, (int & 255)),
 | 
					  b: Math.max(0, int & 255),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getAverageFromBlurhash = (blurhash: string) => {
 | 
					export const getAverageFromBlurhash = (blurhash: string) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
export function compareId (id1: string, id2: string) {
 | 
					export function compareId(id1: string, id2: string) {
 | 
				
			||||||
  if (id1 === id2) {
 | 
					  if (id1 === id2) {
 | 
				
			||||||
    return 0;
 | 
					    return 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,13 +16,10 @@ const obfuscatedCount = (count: number) => {
 | 
				
			|||||||
type Props = {
 | 
					type Props = {
 | 
				
			||||||
  value: number;
 | 
					  value: number;
 | 
				
			||||||
  obfuscate?: boolean;
 | 
					  obfuscate?: boolean;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
export const AnimatedNumber: React.FC<Props> = ({
 | 
					export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
 | 
				
			||||||
  value,
 | 
					 | 
				
			||||||
  obfuscate,
 | 
					 | 
				
			||||||
})=> {
 | 
					 | 
				
			||||||
  const [previousValue, setPreviousValue] = useState(value);
 | 
					  const [previousValue, setPreviousValue] = useState(value);
 | 
				
			||||||
  const [direction, setDirection] = useState<1|-1>(1);
 | 
					  const [direction, setDirection] = useState<1 | -1>(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (previousValue !== value) {
 | 
					  if (previousValue !== value) {
 | 
				
			||||||
    setPreviousValue(value);
 | 
					    setPreviousValue(value);
 | 
				
			||||||
@@ -30,24 +27,45 @@ export const AnimatedNumber: React.FC<Props> = ({
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
 | 
					  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) {
 | 
					  if (reduceMotion) {
 | 
				
			||||||
    return obfuscate ? <>{obfuscatedCount(value)}</> : <ShortNumber value={value} />;
 | 
					    return obfuscate ? (
 | 
				
			||||||
 | 
					      <>{obfuscatedCount(value)}</>
 | 
				
			||||||
 | 
					    ) : (
 | 
				
			||||||
 | 
					      <ShortNumber value={value} />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const styles = [{
 | 
					  const styles = [
 | 
				
			||||||
    key: `${value}`,
 | 
					    {
 | 
				
			||||||
    data: value,
 | 
					      key: `${value}`,
 | 
				
			||||||
    style: { y: spring(0, { damping: 35, stiffness: 400 }) },
 | 
					      data: value,
 | 
				
			||||||
  }];
 | 
					      style: { y: spring(0, { damping: 35, stiffness: 400 }) },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
 | 
					    <TransitionMotion
 | 
				
			||||||
      {items => (
 | 
					      styles={styles}
 | 
				
			||||||
 | 
					      willEnter={willEnter}
 | 
				
			||||||
 | 
					      willLeave={willLeave}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {(items) => (
 | 
				
			||||||
        <span className='animated-number'>
 | 
					        <span className='animated-number'>
 | 
				
			||||||
          {items.map(({ key, data, style }) => (
 | 
					          {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>
 | 
					        </span>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,13 +18,19 @@ export const AvatarOverlay: React.FC<Props> = ({
 | 
				
			|||||||
  baseSize = 36,
 | 
					  baseSize = 36,
 | 
				
			||||||
  overlaySize = 24,
 | 
					  overlaySize = 24,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif);
 | 
					  const { hovering, handleMouseEnter, handleMouseLeave } =
 | 
				
			||||||
  const accountSrc = hovering ? account?.get('avatar') : account?.get('avatar_static');
 | 
					    useHovering(autoPlayGif);
 | 
				
			||||||
  const friendSrc = hovering ? friend?.get('avatar') : friend?.get('avatar_static');
 | 
					  const accountSrc = hovering
 | 
				
			||||||
 | 
					    ? account?.get('avatar')
 | 
				
			||||||
 | 
					    : account?.get('avatar_static');
 | 
				
			||||||
 | 
					  const friendSrc = hovering
 | 
				
			||||||
 | 
					    ? friend?.get('avatar')
 | 
				
			||||||
 | 
					    : friend?.get('avatar_static');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className='account__avatar-overlay' style={{ width: size, height: size }}
 | 
					      className='account__avatar-overlay'
 | 
				
			||||||
 | 
					      style={{ width: size, height: size }}
 | 
				
			||||||
      onMouseEnter={handleMouseEnter}
 | 
					      onMouseEnter={handleMouseEnter}
 | 
				
			||||||
      onMouseLeave={handleMouseLeave}
 | 
					      onMouseLeave={handleMouseLeave}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ type Props = {
 | 
				
			|||||||
  dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
 | 
					  dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
 | 
				
			||||||
  children?: never;
 | 
					  children?: never;
 | 
				
			||||||
  [key: string]: any;
 | 
					  [key: string]: any;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
const Blurhash: React.FC<Props> = ({
 | 
					const Blurhash: React.FC<Props> = ({
 | 
				
			||||||
  hash,
 | 
					  hash,
 | 
				
			||||||
  width = 32,
 | 
					  width = 32,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,15 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Check: React.FC = () => (
 | 
					export const Check: React.FC = () => (
 | 
				
			||||||
  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'>
 | 
					  <svg
 | 
				
			||||||
    <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' />
 | 
					    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>
 | 
					  </svg>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ type Props = {
 | 
				
			|||||||
  width: number;
 | 
					  width: number;
 | 
				
			||||||
  height: number;
 | 
					  height: number;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GIFV: React.FC<Props> = ({
 | 
					export const GIFV: React.FC<Props> = ({
 | 
				
			||||||
  src,
 | 
					  src,
 | 
				
			||||||
@@ -17,19 +17,23 @@ export const GIFV: React.FC<Props> = ({
 | 
				
			|||||||
  width,
 | 
					  width,
 | 
				
			||||||
  height,
 | 
					  height,
 | 
				
			||||||
  onClick,
 | 
					  onClick,
 | 
				
			||||||
})=> {
 | 
					}) => {
 | 
				
			||||||
  const [loading, setLoading] = useState(true);
 | 
					  const [loading, setLoading] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => {
 | 
					  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
 | 
				
			||||||
    setLoading(false);
 | 
					    useCallback(() => {
 | 
				
			||||||
  }, [setLoading]);
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					    }, [setLoading]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleClick: React.MouseEventHandler = useCallback((e) => {
 | 
					  const handleClick: React.MouseEventHandler = useCallback(
 | 
				
			||||||
    if (onClick) {
 | 
					    (e) => {
 | 
				
			||||||
      e.stopPropagation();
 | 
					      if (onClick) {
 | 
				
			||||||
      onClick();
 | 
					        e.stopPropagation();
 | 
				
			||||||
    }
 | 
					        onClick();
 | 
				
			||||||
  }, [onClick]);
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [onClick]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className='gifv' style={{ position: 'relative' }}>
 | 
					    <div className='gifv' style={{ position: 'relative' }}>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,15 @@ type Props = {
 | 
				
			|||||||
  fixedWidth?: boolean;
 | 
					  fixedWidth?: boolean;
 | 
				
			||||||
  children?: never;
 | 
					  children?: never;
 | 
				
			||||||
  [key: string]: any;
 | 
					  [key: string]: any;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
export const Icon: React.FC<Props> = ({ id, className, fixedWidth, ...other }) =>
 | 
					export const Icon: React.FC<Props> = ({
 | 
				
			||||||
  <i className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />;
 | 
					  id,
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  fixedWidth,
 | 
				
			||||||
 | 
					  ...other
 | 
				
			||||||
 | 
					}) => (
 | 
				
			||||||
 | 
					  <i
 | 
				
			||||||
 | 
					    className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
 | 
				
			||||||
 | 
					    {...other}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,13 +25,12 @@ type Props = {
 | 
				
			|||||||
  obfuscateCount?: boolean;
 | 
					  obfuscateCount?: boolean;
 | 
				
			||||||
  href?: string;
 | 
					  href?: string;
 | 
				
			||||||
  ariaHidden: boolean;
 | 
					  ariaHidden: boolean;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
type States = {
 | 
					type States = {
 | 
				
			||||||
  activate: boolean,
 | 
					  activate: boolean;
 | 
				
			||||||
  deactivate: boolean,
 | 
					  deactivate: boolean;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
export class IconButton extends React.PureComponent<Props, States> {
 | 
					export class IconButton extends React.PureComponent<Props, States> {
 | 
				
			||||||
 | 
					 | 
				
			||||||
  static defaultProps = {
 | 
					  static defaultProps = {
 | 
				
			||||||
    size: 18,
 | 
					    size: 18,
 | 
				
			||||||
    active: false,
 | 
					    active: false,
 | 
				
			||||||
@@ -47,7 +46,7 @@ export class IconButton extends React.PureComponent<Props, States> {
 | 
				
			|||||||
    deactivate: false,
 | 
					    deactivate: false,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  UNSAFE_componentWillReceiveProps (nextProps: Props) {
 | 
					  UNSAFE_componentWillReceiveProps(nextProps: Props) {
 | 
				
			||||||
    if (!nextProps.animate) return;
 | 
					    if (!nextProps.animate) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.props.active && !nextProps.active) {
 | 
					    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();
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!this.props.disabled && this.props.onClick != null) {
 | 
					    if (!this.props.disabled && this.props.onClick != null) {
 | 
				
			||||||
@@ -83,7 +82,7 @@ export class IconButton extends React.PureComponent<Props, States> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render() {
 | 
				
			||||||
    const style = {
 | 
					    const style = {
 | 
				
			||||||
      fontSize: `${this.props.size}px`,
 | 
					      fontSize: `${this.props.size}px`,
 | 
				
			||||||
      width: `${this.props.size * 1.28571429}px`,
 | 
					      width: `${this.props.size * 1.28571429}px`,
 | 
				
			||||||
@@ -109,10 +108,7 @@ export class IconButton extends React.PureComponent<Props, States> {
 | 
				
			|||||||
      ariaHidden,
 | 
					      ariaHidden,
 | 
				
			||||||
    } = this.props;
 | 
					    } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const {
 | 
					    const { activate, deactivate } = this.state;
 | 
				
			||||||
      activate,
 | 
					 | 
				
			||||||
      deactivate,
 | 
					 | 
				
			||||||
    } = this.state;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const classes = classNames(className, 'icon-button', {
 | 
					    const classes = classNames(className, 'icon-button', {
 | 
				
			||||||
      active,
 | 
					      active,
 | 
				
			||||||
@@ -130,7 +126,12 @@ export class IconButton extends React.PureComponent<Props, States> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    let contents = (
 | 
					    let contents = (
 | 
				
			||||||
      <React.Fragment>
 | 
					      <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>
 | 
					      </React.Fragment>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -162,5 +163,4 @@ export class IconButton extends React.PureComponent<Props, States> {
 | 
				
			|||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,25 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { Icon } from './icon';
 | 
					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 = {
 | 
					type Props = {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
  count: number;
 | 
					  count: number;
 | 
				
			||||||
  issueBadge: boolean;
 | 
					  issueBadge: boolean;
 | 
				
			||||||
  className: string;
 | 
					  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'>
 | 
					  <i className='icon-with-badge'>
 | 
				
			||||||
    <Icon id={id} fixedWidth className={className} />
 | 
					    <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' />}
 | 
					    {issueBadge && <i className='icon-with-badge__issue-badge' />}
 | 
				
			||||||
  </i>
 | 
					  </i>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,9 +7,14 @@ type Props = {
 | 
				
			|||||||
  srcSet?: string;
 | 
					  srcSet?: string;
 | 
				
			||||||
  blurhash?: string;
 | 
					  blurhash?: string;
 | 
				
			||||||
  className?: 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 [loaded, setLoaded] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleLoad = useCallback(() => {
 | 
					  const handleLoad = useCallback(() => {
 | 
				
			||||||
@@ -17,7 +22,10 @@ export const Image: React.FC<Props> = ({ src, srcSet, blurhash, className }) =>
 | 
				
			|||||||
  }, [setLoaded]);
 | 
					  }, [setLoaded]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={classNames('image', { loaded }, className)} role='presentation'>
 | 
					    <div
 | 
				
			||||||
 | 
					      className={classNames('image', { loaded }, className)}
 | 
				
			||||||
 | 
					      role='presentation'
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      {blurhash && <Blurhash hash={blurhash} className='image__preview' />}
 | 
					      {blurhash && <Blurhash hash={blurhash} className='image__preview' />}
 | 
				
			||||||
      <img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
 | 
					      <img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,10 @@ import { FormattedMessage } from 'react-intl';
 | 
				
			|||||||
export const NotSignedInIndicator: React.FC = () => (
 | 
					export const NotSignedInIndicator: React.FC = () => (
 | 
				
			||||||
  <div className='scrollable scrollable--flex'>
 | 
					  <div className='scrollable scrollable--flex'>
 | 
				
			||||||
    <div className='empty-column-indicator'>
 | 
					    <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>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,13 @@ type Props = {
 | 
				
			|||||||
  label: React.ReactNode;
 | 
					  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 (
 | 
					  return (
 | 
				
			||||||
    <label className='radio-button'>
 | 
					    <label className='radio-button'>
 | 
				
			||||||
      <input
 | 
					      <input
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,20 +4,50 @@ import { injectIntl, defineMessages, InjectedIntl } from 'react-intl';
 | 
				
			|||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  today: { id: 'relative_time.today', defaultMessage: 'today' },
 | 
					  today: { id: 'relative_time.today', defaultMessage: 'today' },
 | 
				
			||||||
  just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
 | 
					  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: { 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: { 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: { 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: { id: 'relative_time.days', defaultMessage: '{number}d' },
 | 
				
			||||||
  days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' },
 | 
					  days_full: {
 | 
				
			||||||
  moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
 | 
					    id: 'relative_time.full.days',
 | 
				
			||||||
  seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
 | 
					    defaultMessage: '{number, plural, one {# day} other {# days}} ago',
 | 
				
			||||||
  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' },
 | 
					  moments_remaining: {
 | 
				
			||||||
  days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
 | 
					    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 = {
 | 
					const dateFormatOptions = {
 | 
				
			||||||
@@ -36,8 +66,8 @@ const shortDateFormatOptions = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const SECOND = 1000;
 | 
					const SECOND = 1000;
 | 
				
			||||||
const MINUTE = 1000 * 60;
 | 
					const MINUTE = 1000 * 60;
 | 
				
			||||||
const HOUR   = 1000 * 60 * 60;
 | 
					const HOUR = 1000 * 60 * 60;
 | 
				
			||||||
const DAY    = 1000 * 60 * 60 * 24;
 | 
					const DAY = 1000 * 60 * 60 * 24;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MAX_DELAY = 2147483647;
 | 
					const MAX_DELAY = 2147483647;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,20 +87,27 @@ const selectUnits = (delta: number) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const getUnitDelay = (units: string) => {
 | 
					const getUnitDelay = (units: string) => {
 | 
				
			||||||
  switch (units) {
 | 
					  switch (units) {
 | 
				
			||||||
  case 'second':
 | 
					    case 'second':
 | 
				
			||||||
    return SECOND;
 | 
					      return SECOND;
 | 
				
			||||||
  case 'minute':
 | 
					    case 'minute':
 | 
				
			||||||
    return MINUTE;
 | 
					      return MINUTE;
 | 
				
			||||||
  case 'hour':
 | 
					    case 'hour':
 | 
				
			||||||
    return HOUR;
 | 
					      return HOUR;
 | 
				
			||||||
  case 'day':
 | 
					    case 'day':
 | 
				
			||||||
    return DAY;
 | 
					      return DAY;
 | 
				
			||||||
  default:
 | 
					    default:
 | 
				
			||||||
    return MAX_DELAY;
 | 
					      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();
 | 
					  const delta = now - date.getTime();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let relativeTime;
 | 
					  let relativeTime;
 | 
				
			||||||
@@ -78,27 +115,49 @@ export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year:
 | 
				
			|||||||
  if (delta < DAY && !timeGiven) {
 | 
					  if (delta < DAY && !timeGiven) {
 | 
				
			||||||
    relativeTime = intl.formatMessage(messages.today);
 | 
					    relativeTime = intl.formatMessage(messages.today);
 | 
				
			||||||
  } else if (delta < 10 * SECOND) {
 | 
					  } 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) {
 | 
					  } else if (delta < 7 * DAY) {
 | 
				
			||||||
    if (delta < MINUTE) {
 | 
					    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) {
 | 
					    } 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) {
 | 
					    } 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 {
 | 
					    } 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) {
 | 
					  } else if (date.getFullYear() === year) {
 | 
				
			||||||
    relativeTime = intl.formatDate(date, shortDateFormatOptions);
 | 
					    relativeTime = intl.formatDate(date, shortDateFormatOptions);
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
 | 
					    relativeTime = intl.formatDate(date, {
 | 
				
			||||||
 | 
					      ...shortDateFormatOptions,
 | 
				
			||||||
 | 
					      year: 'numeric',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return relativeTime;
 | 
					  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;
 | 
					  const delta = date.getTime() - now;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let relativeTime;
 | 
					  let relativeTime;
 | 
				
			||||||
@@ -108,13 +167,21 @@ const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGi
 | 
				
			|||||||
  } else if (delta < 10 * SECOND) {
 | 
					  } else if (delta < 10 * SECOND) {
 | 
				
			||||||
    relativeTime = intl.formatMessage(messages.moments_remaining);
 | 
					    relativeTime = intl.formatMessage(messages.moments_remaining);
 | 
				
			||||||
  } else if (delta < MINUTE) {
 | 
					  } 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) {
 | 
					  } 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) {
 | 
					  } 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 {
 | 
					  } 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;
 | 
					  return relativeTime;
 | 
				
			||||||
@@ -126,78 +193,86 @@ type Props = {
 | 
				
			|||||||
  year: number;
 | 
					  year: number;
 | 
				
			||||||
  futureDate?: boolean;
 | 
					  futureDate?: boolean;
 | 
				
			||||||
  short?: boolean;
 | 
					  short?: boolean;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
type States = {
 | 
					type States = {
 | 
				
			||||||
  now: number;
 | 
					  now: number;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
class RelativeTimestamp extends React.Component<Props, States> {
 | 
					class RelativeTimestamp extends React.Component<Props, States> {
 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					  state = {
 | 
				
			||||||
    now: this.props.intl.now(),
 | 
					    now: this.props.intl.now(),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static defaultProps = {
 | 
					  static defaultProps = {
 | 
				
			||||||
    year: (new Date()).getFullYear(),
 | 
					    year: new Date().getFullYear(),
 | 
				
			||||||
    short: true,
 | 
					    short: true,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _timer: number | undefined;
 | 
					  _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,
 | 
					    // 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.
 | 
					    // 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.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) {
 | 
					    if (this.props.timestamp !== nextProps.timestamp) {
 | 
				
			||||||
      this.setState({ now: this.props.intl.now() });
 | 
					      this.setState({ now: this.props.intl.now() });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentDidMount () {
 | 
					  componentDidMount() {
 | 
				
			||||||
    this._scheduleNextUpdate(this.props, this.state);
 | 
					    this._scheduleNextUpdate(this.props, this.state);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  UNSAFE_componentWillUpdate (nextProps: Props, nextState: States) {
 | 
					  UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) {
 | 
				
			||||||
    this._scheduleNextUpdate(nextProps, nextState);
 | 
					    this._scheduleNextUpdate(nextProps, nextState);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillUnmount () {
 | 
					  componentWillUnmount() {
 | 
				
			||||||
    window.clearTimeout(this._timer);
 | 
					    window.clearTimeout(this._timer);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _scheduleNextUpdate (props: Props, state: States) {
 | 
					  _scheduleNextUpdate(props: Props, state: States) {
 | 
				
			||||||
    window.clearTimeout(this._timer);
 | 
					    window.clearTimeout(this._timer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { timestamp }  = props;
 | 
					    const { timestamp } = props;
 | 
				
			||||||
    const delta          = (new Date(timestamp)).getTime() - state.now;
 | 
					    const delta = new Date(timestamp).getTime() - state.now;
 | 
				
			||||||
    const unitDelay      = getUnitDelay(selectUnits(delta));
 | 
					    const unitDelay = getUnitDelay(selectUnits(delta));
 | 
				
			||||||
    const unitRemainder  = Math.abs(delta % unitDelay);
 | 
					    const unitRemainder = Math.abs(delta % unitDelay);
 | 
				
			||||||
    const updateInterval = 1000 * 10;
 | 
					    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._timer = window.setTimeout(() => {
 | 
				
			||||||
      this.setState({ now: this.props.intl.now() });
 | 
					      this.setState({ now: this.props.intl.now() });
 | 
				
			||||||
    }, delay);
 | 
					    }, delay);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render() {
 | 
				
			||||||
    const { timestamp, intl, year, futureDate, short } = this.props;
 | 
					    const { timestamp, intl, year, futureDate, short } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const timeGiven    = timestamp.includes('T');
 | 
					    const timeGiven = timestamp.includes('T');
 | 
				
			||||||
    const date         = new Date(timestamp);
 | 
					    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 relativeTime = futureDate
 | 
				
			||||||
 | 
					      ? timeRemainingString(intl, date, this.state.now, timeGiven)
 | 
				
			||||||
 | 
					      : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
 | 
					      <time
 | 
				
			||||||
 | 
					        dateTime={timestamp}
 | 
				
			||||||
 | 
					        title={intl.formatDate(date, dateFormatOptions)}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        {relativeTime}
 | 
					        {relativeTime}
 | 
				
			||||||
      </time>
 | 
					      </time>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp);
 | 
					const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
export const PERMISSION_INVITE_USERS      = 0x0000000000010000;
 | 
					export const PERMISSION_INVITE_USERS = 0x0000000000010000;
 | 
				
			||||||
export const PERMISSION_MANAGE_USERS      = 0x0000000000000400;
 | 
					export const PERMISSION_MANAGE_USERS = 0x0000000000000400;
 | 
				
			||||||
export const PERMISSION_MANAGE_FEDERATION = 0x0000000000000020;
 | 
					export const PERMISSION_MANAGE_FEDERATION = 0x0000000000000020;
 | 
				
			||||||
export const PERMISSION_MANAGE_REPORTS    = 0x0000000000000010;
 | 
					export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ if (!HTMLCanvasElement.prototype.toBlob) {
 | 
				
			|||||||
  const BASE64_MARKER = ';base64,';
 | 
					  const BASE64_MARKER = ';base64,';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
 | 
					  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);
 | 
					      const dataURL = this.toDataURL(type, quality);
 | 
				
			||||||
      let data;
 | 
					      let data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,18 +14,18 @@ const initialState = Record<MissedUpdatesState>({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function missedUpdatesReducer(
 | 
					export function missedUpdatesReducer(
 | 
				
			||||||
  state = initialState,
 | 
					  state = initialState,
 | 
				
			||||||
  action: Action<string>,
 | 
					  action: Action<string>
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  switch (action.type) {
 | 
					  switch (action.type) {
 | 
				
			||||||
  case focusApp.type:
 | 
					    case focusApp.type:
 | 
				
			||||||
    return state.set('focused', true).set('unread', 0);
 | 
					      return state.set('focused', true).set('unread', 0);
 | 
				
			||||||
  case unfocusApp.type:
 | 
					    case unfocusApp.type:
 | 
				
			||||||
    return state.set('focused', false);
 | 
					      return state.set('focused', false);
 | 
				
			||||||
  case NOTIFICATIONS_UPDATE:
 | 
					    case NOTIFICATIONS_UPDATE:
 | 
				
			||||||
    return state.get('focused')
 | 
					      return state.get('focused')
 | 
				
			||||||
      ? state
 | 
					        ? state
 | 
				
			||||||
      : state.update('unread', (x) => x + 1);
 | 
					        : state.update('unread', (x) => x + 1);
 | 
				
			||||||
  default:
 | 
					    default:
 | 
				
			||||||
    return state;
 | 
					      return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 easingOutQuint = (
 | 
				
			||||||
const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => {
 | 
					  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 startTime = Date.now();
 | 
				
			||||||
  const offset    = node[key];
 | 
					  const offset = node[key];
 | 
				
			||||||
  const gap       = target - offset;
 | 
					  const gap = target - offset;
 | 
				
			||||||
  const duration  = 1000;
 | 
					  const duration = 1000;
 | 
				
			||||||
  let interrupt   = false;
 | 
					  let interrupt = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const step = () => {
 | 
					  const step = () => {
 | 
				
			||||||
    const elapsed    = Date.now() - startTime;
 | 
					    const elapsed = Date.now() - startTime;
 | 
				
			||||||
    const percentage = elapsed / duration;
 | 
					    const percentage = elapsed / duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (percentage > 1 || interrupt) {
 | 
					    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 scrollRight = (node: Element, position: number) =>
 | 
				
			||||||
export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
 | 
					  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);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,17 +7,21 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const store = configureStore({
 | 
					export const store = configureStore({
 | 
				
			||||||
  reducer: rootReducer,
 | 
					  reducer: rootReducer,
 | 
				
			||||||
  middleware: getDefaultMiddleware =>
 | 
					  middleware: (getDefaultMiddleware) =>
 | 
				
			||||||
    getDefaultMiddleware().concat(
 | 
					    getDefaultMiddleware()
 | 
				
			||||||
      loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }))
 | 
					      .concat(
 | 
				
			||||||
 | 
					        loadingBarMiddleware({
 | 
				
			||||||
 | 
					          promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
      .concat(errorsMiddleware)
 | 
					      .concat(errorsMiddleware)
 | 
				
			||||||
      .concat(soundsMiddleware()),
 | 
					      .concat(soundsMiddleware()),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Infer the `RootState` and `AppDispatch` types from the store itself
 | 
					// 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}
 | 
					// 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 useAppDispatch: () => AppDispatch = useDispatch;
 | 
				
			||||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
 | 
					export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,9 @@ import { RootState } from '..';
 | 
				
			|||||||
const defaultFailSuffix = 'FAIL';
 | 
					const defaultFailSuffix = 'FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
 | 
					export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
 | 
				
			||||||
  ({ dispatch }) => next => action => {
 | 
					  ({ dispatch }) =>
 | 
				
			||||||
 | 
					  (next) =>
 | 
				
			||||||
 | 
					  (action) => {
 | 
				
			||||||
    if (action.type && !action.skipAlert) {
 | 
					    if (action.type && !action.skipAlert) {
 | 
				
			||||||
      const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
 | 
					      const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,29 +3,40 @@ import { Middleware } from 'redux';
 | 
				
			|||||||
import { RootState } from '..';
 | 
					import { RootState } from '..';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Config {
 | 
					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;
 | 
					  const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return ({ dispatch }) => next => (action) => {
 | 
					  return ({ dispatch }) =>
 | 
				
			||||||
    if (action.type && !action.skipLoading) {
 | 
					    (next) =>
 | 
				
			||||||
      const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
 | 
					    (action) => {
 | 
				
			||||||
 | 
					      if (action.type && !action.skipLoading) {
 | 
				
			||||||
 | 
					        const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const isPending = new RegExp(`${PENDING}$`, 'g');
 | 
					        const isPending = new RegExp(`${PENDING}$`, 'g');
 | 
				
			||||||
      const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
 | 
					        const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
 | 
				
			||||||
      const isRejected = new RegExp(`${REJECTED}$`, 'g');
 | 
					        const isRejected = new RegExp(`${REJECTED}$`, 'g');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (action.type.match(isPending)) {
 | 
					        if (action.type.match(isPending)) {
 | 
				
			||||||
        dispatch(showLoading());
 | 
					          dispatch(showLoading());
 | 
				
			||||||
      } else if (action.type.match(isFulfilled) || action.type.match(isRejected)) {
 | 
					        } else if (
 | 
				
			||||||
        dispatch(hideLoading());
 | 
					          action.type.match(isFulfilled) ||
 | 
				
			||||||
 | 
					          action.type.match(isRejected)
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          dispatch(hideLoading());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return next(action);
 | 
					      return next(action);
 | 
				
			||||||
  };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,8 @@ import { Middleware, AnyAction } from 'redux';
 | 
				
			|||||||
import { RootState } from '..';
 | 
					import { RootState } from '..';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AudioSource {
 | 
					interface AudioSource {
 | 
				
			||||||
  src: string
 | 
					  src: string;
 | 
				
			||||||
  type: string
 | 
					  type: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createAudio = (sources: AudioSource[]) => {
 | 
					const createAudio = (sources: AudioSource[]) => {
 | 
				
			||||||
@@ -30,8 +30,11 @@ const play = (audio: HTMLAudioElement) => {
 | 
				
			|||||||
  audio.play();
 | 
					  audio.play();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export  const soundsMiddleware = (): Middleware<Record<string, never>, RootState> => {
 | 
					export const soundsMiddleware = (): Middleware<
 | 
				
			||||||
  const soundCache: {[key: string]: HTMLAudioElement} = {
 | 
					  Record<string, never>,
 | 
				
			||||||
 | 
					  RootState
 | 
				
			||||||
 | 
					> => {
 | 
				
			||||||
 | 
					  const soundCache: { [key: string]: HTMLAudioElement } = {
 | 
				
			||||||
    boop: createAudio([
 | 
					    boop: createAudio([
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        src: '/sounds/boop.ogg',
 | 
					        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;
 | 
					    const sound = action?.meta?.sound;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (sound && soundCache[sound]) {
 | 
					    if (sound && soundCache[sound]) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,16 @@
 | 
				
			|||||||
export const toServerSideType = (columnType: string) => {
 | 
					export const toServerSideType = (columnType: string) => {
 | 
				
			||||||
  switch (columnType) {
 | 
					  switch (columnType) {
 | 
				
			||||||
  case 'home':
 | 
					    case 'home':
 | 
				
			||||||
  case 'notifications':
 | 
					    case 'notifications':
 | 
				
			||||||
  case 'public':
 | 
					    case 'public':
 | 
				
			||||||
  case 'thread':
 | 
					    case 'thread':
 | 
				
			||||||
  case 'account':
 | 
					    case 'account':
 | 
				
			||||||
    return columnType;
 | 
					      return columnType;
 | 
				
			||||||
  default:
 | 
					    default:
 | 
				
			||||||
    if (columnType.indexOf('list:') > -1) {
 | 
					      if (columnType.indexOf('list:') > -1) {
 | 
				
			||||||
      return 'home';
 | 
					        return 'home';
 | 
				
			||||||
    } else {
 | 
					      } else {
 | 
				
			||||||
      return 'public'; // community, account, hashtag
 | 
					        return 'public'; // community, account, hashtag
 | 
				
			||||||
    }
 | 
					      }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,17 +5,8 @@ const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
 | 
				
			|||||||
const buildHashtagPatternRegex = () => {
 | 
					const buildHashtagPatternRegex = () => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    return new RegExp(
 | 
					    return new RegExp(
 | 
				
			||||||
      '(?:^|[^\\/\\)\\w])#((' +
 | 
					      `(?:^|[^\\/\\)\\w])#(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))`,
 | 
				
			||||||
      '[' + WORD + '_]' +
 | 
					      'iu'
 | 
				
			||||||
      '[' + WORD + HASHTAG_SEPARATORS + ']*' +
 | 
					 | 
				
			||||||
      '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
 | 
					 | 
				
			||||||
      '[' + WORD + HASHTAG_SEPARATORS +']*' +
 | 
					 | 
				
			||||||
      '[' + WORD + '_]' +
 | 
					 | 
				
			||||||
      ')|(' +
 | 
					 | 
				
			||||||
      '[' + WORD + '_]*' +
 | 
					 | 
				
			||||||
      '[' + ALPHA + ']' +
 | 
					 | 
				
			||||||
      '[' + WORD + '_]*' +
 | 
					 | 
				
			||||||
      '))', 'iu',
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  } catch {
 | 
					  } catch {
 | 
				
			||||||
    return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
 | 
					    return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
 | 
				
			||||||
@@ -25,17 +16,8 @@ const buildHashtagPatternRegex = () => {
 | 
				
			|||||||
const buildHashtagRegex = () => {
 | 
					const buildHashtagRegex = () => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    return new RegExp(
 | 
					    return new RegExp(
 | 
				
			||||||
      '^((' +
 | 
					      `^(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))$`,
 | 
				
			||||||
      '[' + WORD + '_]' +
 | 
					      'iu'
 | 
				
			||||||
      '[' + WORD + HASHTAG_SEPARATORS + ']*' +
 | 
					 | 
				
			||||||
      '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
 | 
					 | 
				
			||||||
      '[' + WORD + HASHTAG_SEPARATORS +']*' +
 | 
					 | 
				
			||||||
      '[' + WORD + '_]' +
 | 
					 | 
				
			||||||
      ')|(' +
 | 
					 | 
				
			||||||
      '[' + WORD + '_]*' +
 | 
					 | 
				
			||||||
      '[' + ALPHA + ']' +
 | 
					 | 
				
			||||||
      '[' + WORD + '_]*' +
 | 
					 | 
				
			||||||
      '))$', 'iu',
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  } catch {
 | 
					  } catch {
 | 
				
			||||||
    return /^(\w*[a-zA-Z·]\w*)$/i;
 | 
					    return /^(\w*[a-zA-Z·]\w*)$/i;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
				
			|||||||
 * shortNumber(5936);
 | 
					 * shortNumber(5936);
 | 
				
			||||||
 * // => [5.936, 1000, 1]
 | 
					 * // => [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 {
 | 
					export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
				
			||||||
  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
					  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
				
			||||||
    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
					    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
				
			||||||
@@ -38,11 +38,7 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
				
			|||||||
      sourceNumber < TEN_MILLIONS ? 1 : 0,
 | 
					      sourceNumber < TEN_MILLIONS ? 1 : 0,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
  } else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
 | 
					  } else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
 | 
				
			||||||
    return [
 | 
					    return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0];
 | 
				
			||||||
      sourceNumber / DECIMAL_UNITS.BILLION,
 | 
					 | 
				
			||||||
      DECIMAL_UNITS.BILLION,
 | 
					 | 
				
			||||||
      0,
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
					  return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
				
			||||||
@@ -56,7 +52,10 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
				
			|||||||
 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
					 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
				
			||||||
 * // => 1790
 | 
					 * // => 1790
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function pluralReady(sourceNumber: number, division: DecimalUnits): number {
 | 
					export function pluralReady(
 | 
				
			||||||
 | 
					  sourceNumber: number,
 | 
				
			||||||
 | 
					  division: DecimalUnits
 | 
				
			||||||
 | 
					): number {
 | 
				
			||||||
  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
 | 
					  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
 | 
				
			||||||
    return sourceNumber;
 | 
					    return sourceNumber;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
export function uuid(a?: string): string {
 | 
					export function uuid(a?: string): string {
 | 
				
			||||||
  return a
 | 
					  return a
 | 
				
			||||||
    ? (
 | 
					    ? (
 | 
				
			||||||
      (a as any as number) ^
 | 
					        (a as any as number) ^
 | 
				
			||||||
        ((Math.random() * 16) >> ((a as any as number) / 4))
 | 
					        ((Math.random() * 16) >> ((a as any as number) / 4))
 | 
				
			||||||
    ).toString(16)
 | 
					      ).toString(16)
 | 
				
			||||||
    : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
 | 
					    : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -180,10 +180,12 @@
 | 
				
			|||||||
    "@typescript-eslint/parser": "^5.59.5",
 | 
					    "@typescript-eslint/parser": "^5.59.5",
 | 
				
			||||||
    "babel-jest": "^29.5.0",
 | 
					    "babel-jest": "^29.5.0",
 | 
				
			||||||
    "eslint": "^8.39.0",
 | 
					    "eslint": "^8.39.0",
 | 
				
			||||||
 | 
					    "eslint-config-prettier": "^8.8.0",
 | 
				
			||||||
    "eslint-plugin-formatjs": "^4.10.1",
 | 
					    "eslint-plugin-formatjs": "^4.10.1",
 | 
				
			||||||
    "eslint-plugin-import": "~2.27.5",
 | 
					    "eslint-plugin-import": "~2.27.5",
 | 
				
			||||||
    "eslint-plugin-jsdoc": "^43.1.1",
 | 
					    "eslint-plugin-jsdoc": "^43.1.1",
 | 
				
			||||||
    "eslint-plugin-jsx-a11y": "~6.7.1",
 | 
					    "eslint-plugin-jsx-a11y": "~6.7.1",
 | 
				
			||||||
 | 
					    "eslint-plugin-prettier": "^4.2.1",
 | 
				
			||||||
    "eslint-plugin-promise": "~6.1.1",
 | 
					    "eslint-plugin-promise": "~6.1.1",
 | 
				
			||||||
    "eslint-plugin-react": "~7.32.2",
 | 
					    "eslint-plugin-react": "~7.32.2",
 | 
				
			||||||
    "eslint-plugin-react-hooks": "^4.6.0",
 | 
					    "eslint-plugin-react-hooks": "^4.6.0",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								yarn.lock
									
									
									
									
									
								
							@@ -5001,6 +5001,11 @@ escodegen@^2.0.0:
 | 
				
			|||||||
  optionalDependencies:
 | 
					  optionalDependencies:
 | 
				
			||||||
    source-map "~0.6.1"
 | 
					    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:
 | 
					eslint-import-resolver-node@^0.3.7:
 | 
				
			||||||
  version "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"
 | 
					  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"
 | 
					    object.fromentries "^2.0.6"
 | 
				
			||||||
    semver "^6.3.0"
 | 
					    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:
 | 
					eslint-plugin-promise@~6.1.1:
 | 
				
			||||||
  version "6.1.1"
 | 
					  version "6.1.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
 | 
				
			||||||
  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 | 
					  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:
 | 
					fast-glob@^3.2.12, fast-glob@^3.2.9:
 | 
				
			||||||
  version "3.2.12"
 | 
					  version "3.2.12"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
 | 
				
			||||||
  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
 | 
					  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:
 | 
					prettier@^2.8.8:
 | 
				
			||||||
  version "2.8.8"
 | 
					  version "2.8.8"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
 | 
					  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user