Merge commit '51b83ed19536b06ce3f57b260400ecec2d1dd187' into glitch-soc/merge-upstream
This commit is contained in:
		
							
								
								
									
										29
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								.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: { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| module.exports = { | ||||
|   singleQuote: true | ||||
|   singleQuote: true, | ||||
|   jsxSingleQuote: true | ||||
| } | ||||
|   | ||||
| @@ -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) => { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| export function compareId (id1: string, id2: string) { | ||||
| export function compareId(id1: string, id2: string) { | ||||
|   if (id1 === id2) { | ||||
|     return 0; | ||||
|   } | ||||
|   | ||||
| @@ -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> | ||||
|       )} | ||||
|   | ||||
| @@ -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} | ||||
|     > | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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> | ||||
| ); | ||||
|   | ||||
| @@ -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' }}> | ||||
|   | ||||
| @@ -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} | ||||
|   /> | ||||
| ); | ||||
|   | ||||
| @@ -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> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
| ); | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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> | ||||
| ); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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'); | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|     }; | ||||
| }; | ||||
|   | ||||
| @@ -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]) { | ||||
|   | ||||
| @@ -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 | ||||
|       } | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -183,10 +183,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", | ||||
|   | ||||
							
								
								
									
										24
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -5006,6 +5006,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" | ||||
| @@ -5096,6 +5101,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" | ||||
| @@ -5440,6 +5452,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" | ||||
| @@ -9214,6 +9231,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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user