parent
facc7ab03c
commit
96e99e2170
|
@ -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}`,
|
key: `${value}`,
|
||||||
data: value,
|
data: value,
|
||||||
style: { y: spring(0, { damping: 35, stiffness: 400 }) },
|
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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -10,7 +10,7 @@ type Props = {
|
||||||
size: number;
|
size: number;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Avatar: React.FC<Props> = ({
|
export const Avatar: React.FC<Props> = ({
|
||||||
account,
|
account,
|
||||||
|
@ -19,7 +19,8 @@ export const Avatar: React.FC<Props> = ({
|
||||||
inline = false,
|
inline = false,
|
||||||
style: styleFromParent,
|
style: styleFromParent,
|
||||||
}) => {
|
}) => {
|
||||||
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif);
|
const { hovering, handleMouseEnter, handleMouseLeave } =
|
||||||
|
useHovering(autoPlayGif);
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
...styleFromParent,
|
...styleFromParent,
|
||||||
|
@ -29,12 +30,18 @@ export const Avatar: React.FC<Props> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
style.backgroundImage = `url(${account.get(hovering ? 'avatar' : 'avatar_static')})`;
|
style.backgroundImage = `url(${account.get(
|
||||||
|
hovering ? 'avatar' : 'avatar_static'
|
||||||
|
)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('account__avatar', { 'account__avatar-inline': inline }, className)}
|
className={classNames(
|
||||||
|
'account__avatar',
|
||||||
|
{ 'account__avatar-inline': inline },
|
||||||
|
className
|
||||||
|
)}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
style={style}
|
style={style}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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> =
|
||||||
|
useCallback(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [setLoading]);
|
}, [setLoading]);
|
||||||
|
|
||||||
const handleClick: React.MouseEventHandler = useCallback((e) => {
|
const handleClick: React.MouseEventHandler = useCallback(
|
||||||
|
(e) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClick();
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
|
@ -26,13 +26,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,
|
||||||
|
@ -48,7 +47,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) {
|
||||||
|
@ -84,7 +83,7 @@ export class IconButton extends React.PureComponent<Props, States> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
// Hack required for some icons which have an overriden size
|
// Hack required for some icons which have an overriden size
|
||||||
let containerSize = '1.28571429em';
|
let containerSize = '1.28571429em';
|
||||||
if (this.props.style?.fontSize) {
|
if (this.props.style?.fontSize) {
|
||||||
|
@ -120,10 +119,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,
|
||||||
|
@ -141,7 +137,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>
|
||||||
|
)}
|
||||||
{this.props.label}
|
{this.props.label}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
@ -174,5 +175,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 = {
|
||||||
|
@ -70,7 +100,14 @@ const getUnitDelay = (units: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -6,7 +6,7 @@ const LAYOUT_BREAKPOINT = 630;
|
||||||
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
|
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
|
||||||
|
|
||||||
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
|
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
|
||||||
export const layoutFromWindow = (layout_local_setting : string): LayoutType => {
|
export const layoutFromWindow = (layout_local_setting: string): LayoutType => {
|
||||||
switch (layout_local_setting) {
|
switch (layout_local_setting) {
|
||||||
case 'multiple':
|
case 'multiple':
|
||||||
return 'multi-column';
|
return 'multi-column';
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
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;
|
||||||
|
@ -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,15 +3,23 @@ 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 }) =>
|
||||||
|
(next) =>
|
||||||
|
(action) => {
|
||||||
if (action.type && !action.skipLoading) {
|
if (action.type && !action.skipLoading) {
|
||||||
const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
|
const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
|
||||||
|
|
||||||
|
@ -21,7 +29,10 @@ export const loadingBarMiddleware = (config: Config = {}): Middleware<Record<st
|
||||||
|
|
||||||
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 (
|
||||||
|
action.type.match(isFulfilled) ||
|
||||||
|
action.type.match(isRejected)
|
||||||
|
) {
|
||||||
dispatch(hideLoading());
|
dispatch(hideLoading());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]) {
|
||||||
|
|
|
@ -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,3 +1,8 @@
|
||||||
export function uuid(a?: string): string {
|
export function uuid(a?: string): string {
|
||||||
return a ? ((a as any as number) ^ Math.random() * 16 >> (a as any as number) / 4).toString(16) : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
|
return a
|
||||||
|
? (
|
||||||
|
(a as any as number) ^
|
||||||
|
((Math.random() * 16) >> ((a as any as number) / 4))
|
||||||
|
).toString(16)
|
||||||
|
: ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue