Migrate util, types, state, sticker-creator to ESLint

This commit is contained in:
Sidney Keese 2020-09-14 14:56:35 -07:00 committed by Josh Perez
parent 372aa44e49
commit 2ade4acd52
115 changed files with 647 additions and 448 deletions

View file

@ -24,25 +24,19 @@ test/blanket_mocha.js
# TypeScript generated files # TypeScript generated files
ts/**/*.js ts/**/*.js
sticker-creator/**/*.js
**/*.d.ts **/*.d.ts
webpack.config.ts webpack.config.ts
# Temporarily ignored during TSLint transition # Temporarily ignored during TSLint transition
# JIRA: DESKTOP-304 # JIRA: DESKTOP-304
sticker-creator/**/*.ts
sticker-creator/**/*.tsx
ts/*.ts ts/*.ts
ts/components/*.ts
ts/components/*.tsx
ts/components/stickers/** ts/components/stickers/**
ts/shims/** ts/shims/**
ts/sql/** ts/sql/**
ts/state/**
ts/storybook/** ts/storybook/**
ts/styleguide/** ts/styleguide/**
ts/test/** ts/test/**
ts/textsecure/** ts/textsecure/**
ts/types/**
ts/updater/** ts/updater/**
ts/util/**

View file

@ -77,6 +77,8 @@ const rules = {
// Prefer functional components with default params // Prefer functional components with default params
'react/require-default-props': 'off', 'react/require-default-props': 'off',
'jsx-a11y/label-has-associated-control': ['error', { assert: 'either' }],
}; };
module.exports = { module.exports = {
@ -94,7 +96,7 @@ module.exports = {
overrides: [ overrides: [
{ {
files: ['*.ts', '*.tsx'], files: ['ts/**/*.ts', 'ts/**/*.tsx'],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
project: 'tsconfig.json', project: 'tsconfig.json',
@ -113,12 +115,31 @@ module.exports = {
], ],
rules, rules,
}, },
{
files: ['sticker-creator/**/*.ts', 'sticker-creator/**/*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './sticker-creator/tsconfig.json',
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'airbnb-typescript-prettier',
],
rules,
},
{ {
files: ['**/*.stories.tsx', 'ts/build/**'], files: ['**/*.stories.tsx', 'ts/build/**'],
rules: { rules: {
...rules, ...rules,
'import/no-extraneous-dependencies': 'off', 'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
'react/no-array-index-key': 'off', 'react/no-array-index-key': 'off',
}, },
}, },

1
.gitignore vendored
View file

@ -27,6 +27,7 @@ test/test.js
# React / TypeScript # React / TypeScript
ts/**/*.js ts/**/*.js
ts/protobuf/*.d.ts ts/protobuf/*.d.ts
sticker-creator/**/*.js
# CSS Modules # CSS Modules
**/*.scss.d.ts **/*.scss.d.ts

View file

@ -107,6 +107,7 @@
"p-props": "4.0.0", "p-props": "4.0.0",
"p-queue": "6.2.1", "p-queue": "6.2.1",
"pify": "3.0.0", "pify": "3.0.0",
"popper.js": "1.15.0",
"protobufjs": "6.8.6", "protobufjs": "6.8.6",
"proxy-agent": "3.1.1", "proxy-agent": "3.1.1",
"react": "16.8.3", "react": "16.8.3",

View file

@ -9,7 +9,7 @@ import * as styles from './index.scss';
import { PageHeader } from '../elements/PageHeader'; import { PageHeader } from '../elements/PageHeader';
import { useI18n } from '../util/i18n'; import { useI18n } from '../util/i18n';
export const App = () => { export const App: React.ComponentType = () => {
const i18n = useI18n(); const i18n = useI18n();
return ( return (

View file

@ -32,7 +32,7 @@ const getClassName = ({ noMessage, empty }: Props) => {
return styles.main; return styles.main;
}; };
export const AppStage = (props: Props) => { export const AppStage: React.ComponentType<Props> = props => {
const { const {
children, children,
next, next,
@ -67,7 +67,7 @@ export const AppStage = (props: Props) => {
</Button> </Button>
) : null} ) : null}
{addMoreCount > 0 ? ( {addMoreCount > 0 ? (
<Text secondary={true}> <Text secondary>
{i18n('StickerCreator--DropStage--addMore', [addMoreCount])} {i18n('StickerCreator--DropStage--addMore', [addMoreCount])}
</Text> </Text>
) : null} ) : null}
@ -75,7 +75,7 @@ export const AppStage = (props: Props) => {
<Button <Button
className={styles.button} className={styles.button}
onClick={onNext || handleNext} onClick={onNext || handleNext}
primary={true} primary
disabled={!nextActive} disabled={!nextActive}
> >
{nextText || i18n('StickerCreator--AppStage--next')} {nextText || i18n('StickerCreator--AppStage--next')}

View file

@ -7,7 +7,7 @@ import { StickerGrid } from '../../components/StickerGrid';
import { stickersDuck } from '../../store'; import { stickersDuck } from '../../store';
import { useI18n } from '../../util/i18n'; import { useI18n } from '../../util/i18n';
export const DropStage = () => { export const DropStage: React.ComponentType = () => {
const i18n = useI18n(); const i18n = useI18n();
const stickerPaths = stickersDuck.useStickerOrder(); const stickerPaths = stickersDuck.useStickerOrder();
const stickersReady = stickersDuck.useStickersReady(); const stickersReady = stickersDuck.useStickersReady();
@ -17,7 +17,7 @@ export const DropStage = () => {
React.useEffect(() => { React.useEffect(() => {
resetStatus(); resetStatus();
}, []); }, [resetStatus]);
return ( return (
<AppStage next="/add-emojis" nextActive={stickersReady}> <AppStage next="/add-emojis" nextActive={stickersReady}>

View file

@ -6,7 +6,7 @@ import { StickerGrid } from '../../components/StickerGrid';
import { stickersDuck } from '../../store'; import { stickersDuck } from '../../store';
import { useI18n } from '../../util/i18n'; import { useI18n } from '../../util/i18n';
export const EmojiStage = () => { export const EmojiStage: React.ComponentType = () => {
const i18n = useI18n(); const i18n = useI18n();
const emojisReady = stickersDuck.useEmojisReady(); const emojisReady = stickersDuck.useEmojisReady();

View file

@ -11,7 +11,7 @@ import { stickersDuck } from '../../store';
import { useI18n } from '../../util/i18n'; import { useI18n } from '../../util/i18n';
// tslint:disable-next-line max-func-body-length // tslint:disable-next-line max-func-body-length
export const MetaStage = () => { export const MetaStage: React.ComponentType = () => {
const i18n = useI18n(); const i18n = useI18n();
const actions = stickersDuck.useStickerActions(); const actions = stickersDuck.useStickerActions();
const valid = stickersDuck.useAllDataValid(); const valid = stickersDuck.useAllDataValid();
@ -47,19 +47,14 @@ export const MetaStage = () => {
const onConfirm = React.useCallback(() => { const onConfirm = React.useCallback(() => {
history.push('/upload'); history.push('/upload');
}, [setConfirming]); }, []);
const coverFrameClass = isDragActive const coverFrameClass = isDragActive
? styles.coverFrameActive ? styles.coverFrameActive
: styles.coverFrame; : styles.coverFrame;
return ( return (
<AppStage <AppStage onNext={onNext} nextActive={valid} noMessage prev="/add-emojis">
onNext={onNext}
nextActive={valid}
noMessage={true}
prev="/add-emojis"
>
{confirming ? ( {confirming ? (
<ConfirmModal <ConfirmModal
title={i18n('StickerCreator--MetaStage--ConfirmDialog--title')} title={i18n('StickerCreator--MetaStage--ConfirmDialog--title')}

View file

@ -12,7 +12,7 @@ import { stickersDuck } from '../../store';
import { useI18n } from '../../util/i18n'; import { useI18n } from '../../util/i18n';
import { Intl } from '../../../ts/components/Intl'; import { Intl } from '../../../ts/components/Intl';
export const ShareStage = () => { export const ShareStage: React.ComponentType = () => {
const i18n = useI18n(); const i18n = useI18n();
const actions = stickersDuck.useStickerActions(); const actions = stickersDuck.useStickerActions();
const title = stickersDuck.useTitle(); const title = stickersDuck.useTitle();
@ -34,13 +34,13 @@ export const ShareStage = () => {
const handlePrev = React.useCallback(() => { const handlePrev = React.useCallback(() => {
actions.reset(); actions.reset();
history.push('/'); history.push('/');
}, []); }, [actions]);
return ( return (
<AppStage <AppStage
nextText={i18n('StickerCreator--ShareStage--close')} nextText={i18n('StickerCreator--ShareStage--close')}
onNext={handleNext} onNext={handleNext}
nextActive={true} nextActive
prevText={i18n('StickerCreator--ShareStage--createAnother')} prevText={i18n('StickerCreator--ShareStage--createAnother')}
onPrev={handlePrev} onPrev={handlePrev}
> >
@ -66,7 +66,7 @@ export const ShareStage = () => {
/> />
</div> </div>
<div className={styles.row}> <div className={styles.row}>
<Text className={styles.callToAction} center={true}> <Text className={styles.callToAction} center>
<Intl <Intl
i18n={i18n} i18n={i18n}
id="StickerCreator--ShareStage--callToAction" id="StickerCreator--ShareStage--callToAction"

View file

@ -9,13 +9,12 @@ import { Button } from '../../elements/Button';
import { stickersDuck } from '../../store'; import { stickersDuck } from '../../store';
import { encryptAndUpload } from '../../util/preload'; import { encryptAndUpload } from '../../util/preload';
import { useI18n } from '../../util/i18n'; import { useI18n } from '../../util/i18n';
import { Toaster } from '../../components/Toaster';
const handleCancel = () => { const handleCancel = () => {
history.push('/add-meta'); history.push('/add-meta');
}; };
export const UploadStage = () => { export const UploadStage: React.ComponentType = () => {
const i18n = useI18n(); const i18n = useI18n();
const actions = stickersDuck.useStickerActions(); const actions = stickersDuck.useStickerActions();
const cover = stickersDuck.useCover(); const cover = stickersDuck.useCover();
@ -50,10 +49,10 @@ export const UploadStage = () => {
})(); })();
return noop; return noop;
}, [title, author, cover, orderedData]); }, [actions, title, author, cover, orderedData]);
return ( return (
<AppStage empty={true}> <AppStage empty>
<div className={styles.base}> <div className={styles.base}>
<H2>{i18n('StickerCreator--UploadStage--title')}</H2> <H2>{i18n('StickerCreator--UploadStage--title')}</H2>
<Text> <Text>

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from '../elements/StoryRow';
import { ShareButtons } from './ShareButtons';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { StoryRow } from '../elements/StoryRow';
import { ShareButtons } from './ShareButtons';
storiesOf('Sticker Creator/components', module).add('ShareButtons', () => { storiesOf('Sticker Creator/components', module).add('ShareButtons', () => {
const value = text('value', 'https://signal.org'); const value = text('value', 'https://signal.org');

View file

@ -1,3 +1,5 @@
/* eslint-disable max-len */
import * as React from 'react'; import * as React from 'react';
import * as styles from './ShareButtons.scss'; import * as styles from './ShareButtons.scss';
import { useI18n } from '../util/i18n'; import { useI18n } from '../util/i18n';
@ -6,64 +8,67 @@ export type Props = {
value: string; value: string;
}; };
export const ShareButtons = React.memo(({ value }: Props) => { export const ShareButtons: React.ComponentType<Props> = React.memo(
const i18n = useI18n(); ({ value }) => {
const i18n = useI18n();
const buttonPaths = React.useMemo< const buttonPaths = React.useMemo<
Array<[string, string, string, string]> Array<[string, string, string, string]>
>(() => { >(() => {
const packUrl = encodeURIComponent(value); const packUrl = encodeURIComponent(value);
const text = encodeURIComponent( const text = encodeURIComponent(
`${i18n('StickerCreator--ShareStage--socialMessage')} ${value}` `${i18n('StickerCreator--ShareStage--socialMessage')} ${value}`
);
return [
// Facebook
[
i18n('StickerCreator--ShareButtons--facebook'),
'#4267B2',
'M20.155 10.656l-1.506.001c-1.181 0-1.41.561-1.41 1.384v1.816h2.817l-.367 2.845h-2.45V24h-2.937v-7.298h-2.456v-2.845h2.456V11.76c0-2.435 1.487-3.76 3.658-3.76 1.04 0 1.934.077 2.195.112v2.544z',
`https://www.facebook.com/sharer/sharer.php?u=${packUrl}`,
],
// Twitter
[
i18n('StickerCreator--ShareButtons--twitter'),
'#1CA1F2',
'M22.362 12.737c.006.141.01.282.01.425 0 4.337-3.302 9.339-9.34 9.339A9.294 9.294 0 018 21.027c.257.03.518.045.783.045a6.584 6.584 0 004.077-1.405 3.285 3.285 0 01-3.067-2.279 3.312 3.312 0 001.483-.057 3.283 3.283 0 01-2.633-3.218v-.042c.442.246.949.394 1.487.411a3.282 3.282 0 01-1.016-4.383 9.32 9.32 0 006.766 3.43 3.283 3.283 0 015.593-2.994 6.568 6.568 0 002.085-.796 3.299 3.299 0 01-1.443 1.816A6.587 6.587 0 0024 11.038a6.682 6.682 0 01-1.638 1.699',
`https://twitter.com/intent/tweet?text=${text}`,
],
// Pinterest
// [
// i18n('StickerCreator--ShareButtons--pinterest'),
// '#BD081C',
// 'M17.234 19.563c-.992 0-1.926-.536-2.245-1.146 0 0-.534 2.118-.646 2.527-.398 1.444-1.569 2.889-1.66 3.007-.063.083-.203.057-.218-.052-.025-.184-.324-2.007.028-3.493l1.182-5.008s-.293-.587-.293-1.454c0-1.362.789-2.379 1.772-2.379.836 0 1.239.628 1.239 1.38 0 .84-.535 2.097-.811 3.261-.231.975.489 1.77 1.451 1.77 1.74 0 2.913-2.236 2.913-4.886 0-2.014-1.356-3.522-3.824-3.522-2.787 0-4.525 2.079-4.525 4.402 0 .8.237 1.365.607 1.802.17.201.194.282.132.512-.045.17-.145.576-.188.738-.061.233-.249.316-.46.23-1.283-.524-1.882-1.931-1.882-3.511C9.806 11.13 12.008 8 16.374 8c3.51 0 5.819 2.538 5.819 5.265 0 3.605-2.005 6.298-4.959 6.298',
// `https://pinterest.com/pin/create/button/?url=${packUrl}`,
// ],
// Whatsapp
[
i18n('StickerCreator--ShareButtons--whatsapp'),
'#25D366',
'M16.033 23.862h-.003a7.914 7.914 0 01-3.79-.965L8.035 24l1.126-4.109a7.907 7.907 0 01-1.059-3.964C8.104 11.556 11.661 8 16.033 8c2.121 0 4.113.826 5.61 2.325a7.878 7.878 0 012.321 5.609c-.002 4.371-3.56 7.928-7.931 7.928zm3.88-5.101c-.165.463-.957.885-1.338.942a2.727 2.727 0 01-1.248-.078 11.546 11.546 0 01-1.13-.418c-1.987-.858-3.286-2.859-3.385-2.991-.1-.132-.81-1.074-.81-2.049 0-.975.513-1.455.695-1.653a.728.728 0 01.528-.248c.132 0 .264.001.38.007.122.006.285-.046.446.34.165.397.56 1.372.61 1.471.05.099.083.215.017.347-.066.132-.1.215-.198.331-.1.115-.208.258-.297.347-.1.098-.203.206-.087.404.116.198.513.847 1.102 1.372.757.675 1.396.884 1.594.984.198.099.314.082.429-.05.116-.132.496-.578.628-.777.132-.198.264-.165.446-.099.18.066 1.156.545 1.354.645.198.099.33.148.38.231.049.083.049.479-.116.942zm-3.877-9.422c-3.636 0-6.594 2.956-6.595 6.589 0 1.245.348 2.458 1.008 3.507l.157.249-.666 2.432 2.495-.654.24.142a6.573 6.573 0 003.355.919h.003a6.6 6.6 0 006.592-6.59 6.55 6.55 0 00-1.93-4.662 6.549 6.549 0 00-4.66-1.932z',
`https://wa.me?text=${text}`,
],
];
}, [i18n, value]);
return (
<div className={styles.container}>
{buttonPaths.map(([title, fill, path, url]) => (
<button
type="button"
key={path}
className={styles.button}
onClick={() => window.open(url)}
title={title}
>
<svg width={32} height={32}>
<circle cx="16" cy="16" r="16" fill={fill} />
<path d={path} fill="#FFF" fillRule="evenodd" />
</svg>
</button>
))}
</div>
); );
}
return [ );
// Facebook
[
i18n('StickerCreator--ShareButtons--facebook'),
'#4267B2',
'M20.155 10.656l-1.506.001c-1.181 0-1.41.561-1.41 1.384v1.816h2.817l-.367 2.845h-2.45V24h-2.937v-7.298h-2.456v-2.845h2.456V11.76c0-2.435 1.487-3.76 3.658-3.76 1.04 0 1.934.077 2.195.112v2.544z',
`https://www.facebook.com/sharer/sharer.php?u=${packUrl}`,
],
// Twitter
[
i18n('StickerCreator--ShareButtons--twitter'),
'#1CA1F2',
'M22.362 12.737c.006.141.01.282.01.425 0 4.337-3.302 9.339-9.34 9.339A9.294 9.294 0 018 21.027c.257.03.518.045.783.045a6.584 6.584 0 004.077-1.405 3.285 3.285 0 01-3.067-2.279 3.312 3.312 0 001.483-.057 3.283 3.283 0 01-2.633-3.218v-.042c.442.246.949.394 1.487.411a3.282 3.282 0 01-1.016-4.383 9.32 9.32 0 006.766 3.43 3.283 3.283 0 015.593-2.994 6.568 6.568 0 002.085-.796 3.299 3.299 0 01-1.443 1.816A6.587 6.587 0 0024 11.038a6.682 6.682 0 01-1.638 1.699',
`https://twitter.com/intent/tweet?text=${text}`,
],
// Pinterest
// [
// i18n('StickerCreator--ShareButtons--pinterest'),
// '#BD081C',
// 'M17.234 19.563c-.992 0-1.926-.536-2.245-1.146 0 0-.534 2.118-.646 2.527-.398 1.444-1.569 2.889-1.66 3.007-.063.083-.203.057-.218-.052-.025-.184-.324-2.007.028-3.493l1.182-5.008s-.293-.587-.293-1.454c0-1.362.789-2.379 1.772-2.379.836 0 1.239.628 1.239 1.38 0 .84-.535 2.097-.811 3.261-.231.975.489 1.77 1.451 1.77 1.74 0 2.913-2.236 2.913-4.886 0-2.014-1.356-3.522-3.824-3.522-2.787 0-4.525 2.079-4.525 4.402 0 .8.237 1.365.607 1.802.17.201.194.282.132.512-.045.17-.145.576-.188.738-.061.233-.249.316-.46.23-1.283-.524-1.882-1.931-1.882-3.511C9.806 11.13 12.008 8 16.374 8c3.51 0 5.819 2.538 5.819 5.265 0 3.605-2.005 6.298-4.959 6.298',
// `https://pinterest.com/pin/create/button/?url=${packUrl}`,
// ],
// Whatsapp
[
i18n('StickerCreator--ShareButtons--whatsapp'),
'#25D366',
'M16.033 23.862h-.003a7.914 7.914 0 01-3.79-.965L8.035 24l1.126-4.109a7.907 7.907 0 01-1.059-3.964C8.104 11.556 11.661 8 16.033 8c2.121 0 4.113.826 5.61 2.325a7.878 7.878 0 012.321 5.609c-.002 4.371-3.56 7.928-7.931 7.928zm3.88-5.101c-.165.463-.957.885-1.338.942a2.727 2.727 0 01-1.248-.078 11.546 11.546 0 01-1.13-.418c-1.987-.858-3.286-2.859-3.385-2.991-.1-.132-.81-1.074-.81-2.049 0-.975.513-1.455.695-1.653a.728.728 0 01.528-.248c.132 0 .264.001.38.007.122.006.285-.046.446.34.165.397.56 1.372.61 1.471.05.099.083.215.017.347-.066.132-.1.215-.198.331-.1.115-.208.258-.297.347-.1.098-.203.206-.087.404.116.198.513.847 1.102 1.372.757.675 1.396.884 1.594.984.198.099.314.082.429-.05.116-.132.496-.578.628-.777.132-.198.264-.165.446-.099.18.066 1.156.545 1.354.645.198.099.33.148.38.231.049.083.049.479-.116.942zm-3.877-9.422c-3.636 0-6.594 2.956-6.595 6.589 0 1.245.348 2.458 1.008 3.507l.157.249-.666 2.432 2.495-.654.24.142a6.573 6.573 0 003.355.919h.003a6.6 6.6 0 006.592-6.59 6.55 6.55 0 00-1.93-4.662 6.549 6.549 0 00-4.66-1.932z',
`https://wa.me?text=${text}`,
],
];
}, [i18n, value]);
return (
<div className={styles.container}>
{buttonPaths.map(([title, fill, path, url]) => (
<button
key={path}
className={styles.button}
onClick={() => window.open(url)}
title={title}
>
<svg width={32} height={32}>
<circle cx="16" cy="16" r="16" fill={fill} />
<path d={path} fill="#FFF" fillRule="evenodd" />
</svg>
</button>
))}
</div>
);
});

View file

@ -1,11 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from '../elements/StoryRow';
import { StickerFrame } from './StickerFrame';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean, select, text } from '@storybook/addon-knobs'; import { boolean, select, text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { StoryRow } from '../elements/StoryRow';
import { StickerFrame } from './StickerFrame';
storiesOf('Sticker Creator/components', module).add('StickerFrame', () => { storiesOf('Sticker Creator/components', module).add('StickerFrame', () => {
const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp'); const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp');
const showGuide = boolean('show guide', true); const showGuide = boolean('show guide', true);
@ -16,7 +16,7 @@ storiesOf('Sticker Creator/components', module).add('StickerFrame', () => {
const [emoji, setEmoji] = React.useState(undefined); const [emoji, setEmoji] = React.useState(undefined);
return ( return (
<StoryRow top={true}> <StoryRow top>
<StickerFrame <StickerFrame
id="1337" id="1337"
emojiData={emoji} emojiData={emoji}

View file

@ -31,7 +31,13 @@ export type Props = Partial<
readonly image?: string; readonly image?: string;
readonly mode?: Mode; readonly mode?: Mode;
readonly showGuide?: boolean; readonly showGuide?: boolean;
onPickEmoji?({ id: string, emoji: EmojiPickData }): unknown; onPickEmoji?({
id,
emoji,
}: {
id: string;
emoji: EmojiPickDataType;
}): unknown;
onRemove?(id: string): unknown; onRemove?(id: string): unknown;
}; };
@ -180,7 +186,8 @@ export const StickerFrame = React.memo(
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
ref={rootRef} ref={rootRef}
> >
{mode !== 'add' ? ( {// eslint-disable-next-line no-nested-ternary
mode !== 'add' ? (
image ? ( image ? (
<ImageHandle src={image} /> <ImageHandle src={image} />
) : ( ) : (
@ -191,14 +198,11 @@ export const StickerFrame = React.memo(
<div className={styles.guide} /> <div className={styles.guide} />
) : null} ) : null}
{mode === 'add' && onDrop ? ( {mode === 'add' && onDrop ? (
<DropZone <DropZone onDrop={onDrop} inner onDragActive={setDragActive} />
onDrop={onDrop}
inner={true}
onDragActive={setDragActive}
/>
) : null} ) : null}
{mode === 'removable' ? ( {mode === 'removable' ? (
<button <button
type="button"
className={styles.closeButton} className={styles.closeButton}
onClick={handleRemove} onClick={handleRemove}
// Reverse the mouseenter/leave logic for the remove button so // Reverse the mouseenter/leave logic for the remove button so
@ -214,6 +218,7 @@ export const StickerFrame = React.memo(
<PopperReference> <PopperReference>
{({ ref }) => ( {({ ref }) => (
<button <button
type="button"
ref={ref} ref={ref}
className={styles.emojiButton} className={styles.emojiButton}
onClick={handleToggleEmojiPicker} onClick={handleToggleEmojiPicker}

View file

@ -56,7 +56,6 @@ const InnerGrid = SortableContainer(
const webp = await convertToWebp(path); const webp = await convertToWebp(path);
actions.addWebp(webp); actions.addWebp(webp);
} catch (e) { } catch (e) {
// @ts-ignore
window.log.error('Error processing image:', e); window.log.error('Error processing image:', e);
actions.removeSticker(path); actions.removeSticker(path);
actions.addToast({ actions.addToast({
@ -114,7 +113,7 @@ export const StickerGrid = SortableContainer((props: Props) => {
ids={ids} ids={ids}
axis="xy" axis="xy"
onSortEnd={handleSortEnd} onSortEnd={handleSortEnd}
useDragHandle={true} useDragHandle
/> />
); );
}); });

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from '../elements/StoryRow';
import { StickerPackPreview } from './StickerPackPreview';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { StoryRow } from '../elements/StoryRow';
import { StickerPackPreview } from './StickerPackPreview';
storiesOf('Sticker Creator/components', module).add( storiesOf('Sticker Creator/components', module).add(
'StickerPackPreview', 'StickerPackPreview',
() => { () => {
@ -14,7 +14,7 @@ storiesOf('Sticker Creator/components', module).add(
const images = React.useMemo(() => Array(39).fill(image), [image]); const images = React.useMemo(() => Array(39).fill(image), [image]);
return ( return (
<StoryRow top={true}> <StoryRow top>
<StickerPackPreview images={images} title={title} author={author} /> <StickerPackPreview images={images} title={title} author={author} />
</StoryRow> </StoryRow>
); );

View file

@ -19,8 +19,8 @@ export const StickerPackPreview = React.memo(
</div> </div>
<div className={styles.scroller}> <div className={styles.scroller}>
<div className={styles.grid}> <div className={styles.grid}>
{images.map((src, id) => ( {images.map(src => (
<img key={id} className={styles.sticker} src={src} alt={src} /> <img key={src} className={styles.sticker} src={src} alt={src} />
))} ))}
</div> </div>
</div> </div>

View file

@ -1,16 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import { debounce, dropRight } from 'lodash'; import { debounce, dropRight } from 'lodash';
import { StoryRow } from '../elements/StoryRow';
import { Toaster } from './Toaster';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text as textKnob } from '@storybook/addon-knobs'; import { text as textKnob } from '@storybook/addon-knobs';
import { StoryRow } from '../elements/StoryRow';
import { Toaster } from './Toaster';
storiesOf('Sticker Creator/components', module).add('Toaster', () => { storiesOf('Sticker Creator/components', module).add('Toaster', () => {
const inputText = textKnob('Slices', ['error 1', 'error 2'].join('|')); const inputText = textKnob('Slices', ['error 1', 'error 2'].join('|'));
const initialState = React.useMemo(() => inputText.split('|'), [inputText]); const initialState = React.useMemo(() => inputText.split('|'), [inputText]);
const [state, setState] = React.useState(initialState); const [state, setState] = React.useState(initialState);
// TODO not sure how to fix this
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleDismiss = React.useCallback( const handleDismiss = React.useCallback(
// Debounce is required here since auto-dismiss is asynchronously called // Debounce is required here since auto-dismiss is asynchronously called
// from multiple rendered instances (multiple themes) // from multiple rendered instances (multiple themes)

1
sticker-creator/custom.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module '*.scss';

View file

@ -1,11 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { Button } from './Button';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { Button } from './Button';
storiesOf('Sticker Creator/elements', module).add('Button', () => { storiesOf('Sticker Creator/elements', module).add('Button', () => {
const onClick = action('onClick'); const onClick = action('onClick');
const child = text('text', 'foo bar'); const child = text('text', 'foo bar');
@ -13,12 +13,12 @@ storiesOf('Sticker Creator/elements', module).add('Button', () => {
return ( return (
<> <>
<StoryRow> <StoryRow>
<Button onClick={onClick} primary={true}> <Button onClick={onClick} primary>
{child} {child}
</Button> </Button>
</StoryRow> </StoryRow>
<StoryRow> <StoryRow>
<Button onClick={onClick} primary={true} disabled={true}> <Button onClick={onClick} primary disabled>
{child} {child}
</Button> </Button>
</StoryRow> </StoryRow>
@ -26,27 +26,27 @@ storiesOf('Sticker Creator/elements', module).add('Button', () => {
<Button onClick={onClick}>{child}</Button> <Button onClick={onClick}>{child}</Button>
</StoryRow> </StoryRow>
<StoryRow> <StoryRow>
<Button onClick={onClick} disabled={true}> <Button onClick={onClick} disabled>
{child} {child}
</Button> </Button>
</StoryRow> </StoryRow>
<StoryRow> <StoryRow>
<Button onClick={onClick} primary={true} pill={true}> <Button onClick={onClick} primary pill>
{child} {child}
</Button> </Button>
</StoryRow> </StoryRow>
<StoryRow> <StoryRow>
<Button onClick={onClick} primary={true} pill={true} disabled={true}> <Button onClick={onClick} primary pill disabled>
{child} {child}
</Button> </Button>
</StoryRow> </StoryRow>
<StoryRow> <StoryRow>
<Button onClick={onClick} pill={true}> <Button onClick={onClick} pill>
{child} {child}
</Button> </Button>
</StoryRow> </StoryRow>
<StoryRow> <StoryRow>
<Button onClick={onClick} pill={true} disabled={true}> <Button onClick={onClick} pill disabled>
{child} {child}
</Button> </Button>
</StoryRow> </StoryRow>

View file

@ -3,10 +3,8 @@ import * as classnames from 'classnames';
import * as styles from './Button.scss'; import * as styles from './Button.scss';
export type Props = React.HTMLProps<HTMLButtonElement> & { export type Props = React.HTMLProps<HTMLButtonElement> & {
className?: string;
pill?: boolean; pill?: boolean;
primary?: boolean; primary?: boolean;
children: React.ReactNode;
}; };
const getClassName = ({ primary, pill }: Props) => { const getClassName = ({ primary, pill }: Props) => {
@ -25,12 +23,15 @@ const getClassName = ({ primary, pill }: Props) => {
return styles.base; return styles.base;
}; };
export const Button = (props: Props) => { export const Button: React.ComponentType<Props> = ({
const { className, pill, primary, children, ...otherProps } = props; className,
children,
...otherProps
}) => {
return ( return (
<button <button
className={classnames(getClassName(props), className)} type="button"
className={classnames(getClassName(otherProps), className)}
{...otherProps} {...otherProps}
> >
{children} {children}

View file

@ -1,11 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { ConfirmDialog } from './ConfirmDialog';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { StoryRow } from './StoryRow';
import { ConfirmDialog } from './ConfirmDialog';
storiesOf('Sticker Creator/elements', module).add('ConfirmDialog', () => { storiesOf('Sticker Creator/elements', module).add('ConfirmDialog', () => {
const title = text('title', 'Foo bar banana baz?'); const title = text('title', 'Foo bar banana baz?');
const child = text( const child = text(

View file

@ -11,14 +11,14 @@ export type Props = {
readonly onCancel: () => unknown; readonly onCancel: () => unknown;
}; };
export const ConfirmDialog = ({ export const ConfirmDialog: React.ComponentType<Props> = ({
title, title,
children, children,
confirm, confirm,
cancel, cancel,
onConfirm, onConfirm,
onCancel, onCancel,
}: Props) => { }) => {
const i18n = useI18n(); const i18n = useI18n();
const cancelText = cancel || i18n('StickerCreator--ConfirmDialog--cancel'); const cancelText = cancel || i18n('StickerCreator--ConfirmDialog--cancel');
@ -27,10 +27,14 @@ export const ConfirmDialog = ({
<h1 className={styles.title}>{title}</h1> <h1 className={styles.title}>{title}</h1>
<p className={styles.text}>{children}</p> <p className={styles.text}>{children}</p>
<div className={styles.bottom}> <div className={styles.bottom}>
<button className={styles.button} onClick={onCancel}> <button type="button" className={styles.button} onClick={onCancel}>
{cancelText} {cancelText}
</button> </button>
<button className={styles.buttonPrimary} onClick={onConfirm}> <button
type="button"
className={styles.buttonPrimary}
onClick={onConfirm}
>
{confirm} {confirm}
</button> </button>
</div> </div>

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { CopyText } from './CopyText';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { CopyText } from './CopyText';
storiesOf('Sticker Creator/elements', module).add('CopyText', () => { storiesOf('Sticker Creator/elements', module).add('CopyText', () => {
const label = text('label', 'foo bar'); const label = text('label', 'foo bar');
const value = text('value', 'foo bar'); const value = text('value', 'foo bar');

View file

@ -10,27 +10,29 @@ export type Props = {
onCopy?: () => unknown; onCopy?: () => unknown;
}; };
export const CopyText = React.memo(({ label, onCopy, value }: Props) => { export const CopyText: React.ComponentType<Props> = React.memo(
const i18n = useI18n(); ({ label, onCopy, value }) => {
const handleClick = React.useCallback(() => { const i18n = useI18n();
copy(value); const handleClick = React.useCallback(() => {
if (onCopy) { copy(value);
onCopy(); if (onCopy) {
} onCopy();
}, [onCopy, value]); }
}, [onCopy, value]);
return ( return (
<div className={styles.container}> <div className={styles.container}>
<input <input
type="text" type="text"
className={styles.input} className={styles.input}
value={value} value={value}
aria-label={label} aria-label={label}
readOnly={true} readOnly
/> />
<Button onClick={handleClick}> <Button onClick={handleClick}>
{i18n('StickerCreator--CopyText--button')} {i18n('StickerCreator--CopyText--button')}
</Button> </Button>
</div> </div>
); );
}); }
);

View file

@ -1,9 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { DropZone } from './DropZone';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { DropZone } from './DropZone';
storiesOf('Sticker Creator/elements', module).add('DropZone', () => { storiesOf('Sticker Creator/elements', module).add('DropZone', () => {
return <DropZone onDrop={action('onDrop')} />; return <DropZone onDrop={action('onDrop')} />;
}); });

View file

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone, FileWithPath } from 'react-dropzone';
import * as styles from './DropZone.scss'; import * as styles from './DropZone.scss';
import { useI18n } from '../util/i18n'; import { useI18n } from '../util/i18n';
@ -21,12 +21,12 @@ const getClassName = ({ inner }: Props, isDragActive: boolean) => {
return styles.standalone; return styles.standalone;
}; };
export const DropZone = (props: Props) => { export const DropZone: React.ComponentType<Props> = props => {
const { inner, onDrop, onDragActive } = props; const { inner, onDrop, onDragActive } = props;
const i18n = useI18n(); const i18n = useI18n();
const handleDrop = React.useCallback( const handleDrop = React.useCallback(
files => { (files: ReadonlyArray<FileWithPath>) => {
onDrop(files.map(({ path }) => path)); onDrop(files.map(({ path }) => path));
}, },
[onDrop] [onDrop]

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { LabeledCheckbox } from './LabeledCheckbox';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { LabeledCheckbox } from './LabeledCheckbox';
storiesOf('Sticker Creator/elements', module).add('Labeled Checkbox', () => { storiesOf('Sticker Creator/elements', module).add('Labeled Checkbox', () => {
const child = text('label', 'foo bar'); const child = text('label', 'foo bar');
const [checked, setChecked] = React.useState(false); const [checked, setChecked] = React.useState(false);

View file

@ -17,7 +17,9 @@ const checkSvg = (
export const LabeledCheckbox = React.memo( export const LabeledCheckbox = React.memo(
({ children, value, onChange }: Props) => { ({ children, value, onChange }: Props) => {
const handleChange = React.useCallback(() => { const handleChange = React.useCallback(() => {
onChange(!value); if (onChange !== undefined) {
onChange(!value);
}
}, [onChange, value]); }, [onChange, value]);
const className = value ? styles.checkboxChecked : styles.checkbox; const className = value ? styles.checkboxChecked : styles.checkbox;

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { LabeledInput } from './LabeledInput';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { LabeledInput } from './LabeledInput';
storiesOf('Sticker Creator/elements', module).add('LabeledInput', () => { storiesOf('Sticker Creator/elements', module).add('LabeledInput', () => {
const child = text('label', 'foo bar'); const child = text('label', 'foo bar');
const placeholder = text('placeholder', 'foo bar'); const placeholder = text('placeholder', 'foo bar');

View file

@ -13,7 +13,9 @@ export const LabeledInput = React.memo(
({ children, value, placeholder, onChange }: Props) => { ({ children, value, placeholder, onChange }: Props) => {
const handleChange = React.useCallback( const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.currentTarget.value); if (onChange !== undefined) {
onChange(e.currentTarget.value);
}
}, },
[onChange] [onChange]
); );

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { MessageBubble } from './MessageBubble';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { number, text } from '@storybook/addon-knobs'; import { number, text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { MessageBubble } from './MessageBubble';
storiesOf('Sticker Creator/elements', module).add('MessageBubble', () => { storiesOf('Sticker Creator/elements', module).add('MessageBubble', () => {
const child = text('text', 'Foo bar banana baz'); const child = text('text', 'Foo bar banana baz');
const minutesAgo = number('minutesAgo', 3); const minutesAgo = number('minutesAgo', 3);

View file

@ -6,7 +6,10 @@ export type Props = Pick<MessageMetaProps, 'minutesAgo'> & {
children: React.ReactNode; children: React.ReactNode;
}; };
export const MessageBubble = ({ children, minutesAgo }: Props) => { export const MessageBubble: React.ComponentType<Props> = ({
children,
minutesAgo,
}) => {
return ( return (
<div className={styles.base}> <div className={styles.base}>
{children} {children}

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { MessageSticker } from './MessageSticker';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { number, text } from '@storybook/addon-knobs'; import { number, text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { MessageSticker } from './MessageSticker';
storiesOf('Sticker Creator/elements', module).add('MessageSticker', () => { storiesOf('Sticker Creator/elements', module).add('MessageSticker', () => {
const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp'); const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp');
const minutesAgo = number('minutesAgo', 3); const minutesAgo = number('minutesAgo', 3);

View file

@ -6,7 +6,11 @@ export type Props = MessageMetaProps & {
image: string; image: string;
}; };
export const MessageSticker = ({ image, kind, minutesAgo }: Props) => { export const MessageSticker: React.ComponentType<Props> = ({
image,
kind,
minutesAgo,
}) => {
return ( return (
<div className={styles.base}> <div className={styles.base}>
<img src={image} alt="Sticker" className={styles.image} /> <img src={image} alt="Sticker" className={styles.image} />

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { PageHeader } from './PageHeader';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { PageHeader } from './PageHeader';
storiesOf('Sticker Creator/elements', module).add('PageHeader', () => { storiesOf('Sticker Creator/elements', module).add('PageHeader', () => {
const child = text('text', 'foo bar'); const child = text('text', 'foo bar');

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { ProgressBar } from './ProgressBar';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { number } from '@storybook/addon-knobs'; import { number } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { ProgressBar } from './ProgressBar';
storiesOf('Sticker Creator/elements', module).add('ProgressBar', () => { storiesOf('Sticker Creator/elements', module).add('ProgressBar', () => {
const count = number('count', 5); const count = number('count', 5);
const total = number('total', 10); const total = number('total', 10);

View file

@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { StickerPreview } from './StickerPreview';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { StickerPreview } from './StickerPreview';
storiesOf('Sticker Creator/elements', module).add('StickerPreview', () => { storiesOf('Sticker Creator/elements', module).add('StickerPreview', () => {
const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp'); const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp');

View file

@ -1,8 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as styles from './StoryRow.scss'; import * as styles from './StoryRow.scss';
export type Props = { type Props = {
children: React.ReactChild;
left?: boolean; left?: boolean;
right?: boolean; right?: boolean;
top?: boolean; top?: boolean;
@ -29,6 +28,7 @@ const getClassName = ({ left, right, top, bottom }: Props) => {
return styles.base; return styles.base;
}; };
export const StoryRow = (props: Props) => ( export const StoryRow: React.ComponentType<Props> = ({
<div className={getClassName(props)}>{props.children}</div> children,
); ...props
}) => <div className={getClassName(props)}>{children}</div>;

View file

@ -1,11 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { StoryRow } from './StoryRow';
import { Toast } from './Toast';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { StoryRow } from './StoryRow';
import { Toast } from './Toast';
storiesOf('Sticker Creator/elements', module).add('Toast', () => { storiesOf('Sticker Creator/elements', module).add('Toast', () => {
const child = text('text', 'foo bar'); const child = text('text', 'foo bar');

View file

@ -7,7 +7,11 @@ export type Props = React.HTMLProps<HTMLButtonElement> & {
}; };
export const Toast = React.memo(({ children, className, ...rest }: Props) => ( export const Toast = React.memo(({ children, className, ...rest }: Props) => (
<button className={classNames(styles.base, className)} {...rest}> <button
type="button"
className={classNames(styles.base, className)}
{...rest}
>
{children} {children}
</button> </button>
)); ));

View file

@ -1,27 +1,29 @@
import * as React from 'react'; /* eslint-disable no-script-url, jsx-a11y/anchor-is-valid */
import { StoryRow } from './StoryRow';
import { H1, H2, Text } from './Typography';
import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { H1, H2, Text } from './Typography';
storiesOf('Sticker Creator/elements', module).add('Typography', () => { storiesOf('Sticker Creator/elements', module).add('Typography', () => {
const child = text('text', 'foo bar'); const child = text('text', 'foo bar');
return ( return (
<> <>
<StoryRow left={true}> <StoryRow left>
<H1>{child}</H1> <H1>{child}</H1>
</StoryRow> </StoryRow>
<StoryRow left={true}> <StoryRow left>
<H2>{child}</H2> <H2>{child}</H2>
</StoryRow> </StoryRow>
<StoryRow left={true}> <StoryRow left>
<Text> <Text>
{child} {child} {child} {child} {child} {child} {child} {child}
</Text> </Text>
</StoryRow> </StoryRow>
<StoryRow left={true}> <StoryRow left>
<Text> <Text>
{child} {child} {child} {child}{' '} {child} {child} {child} {child}{' '}
<a href="javascript: void 0;"> <a href="javascript: void 0;">

View file

@ -35,7 +35,6 @@ export const Text = React.memo(
children, children,
className, className,
center, center,
wide,
secondary, secondary,
...rest ...rest
}: Props & ParagraphProps) => ( }: Props & ParagraphProps) => (

View file

@ -4,5 +4,6 @@ import { Root } from './root';
const root = document.getElementById('root'); const root = document.getElementById('root');
// eslint-disable-next-line no-console
console.log('Sticker Creator: Starting root'); console.log('Sticker Creator: Starting root');
render(<Root />, root); render(<Root />, root);

View file

@ -1,4 +1,3 @@
// tslint:disable-next-line no-submodule-imports
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import * as React from 'react'; import * as React from 'react';
import { Provider as ReduxProvider } from 'react-redux'; import { Provider as ReduxProvider } from 'react-redux';
@ -8,7 +7,12 @@ import { history } from './util/history';
import { store } from './store'; import { store } from './store';
import { I18n } from './util/i18n'; import { I18n } from './util/i18n';
// @ts-ignore declare global {
interface Window {
localeMessages: { [key: string]: { message: string } };
}
}
const { localeMessages } = window; const { localeMessages } = window;
const ColdRoot = () => ( const ColdRoot = () => (

View file

@ -1,4 +1,4 @@
// tslint:disable no-dynamic-delete /* eslint-disable no-param-reassign */
import { useMemo } from 'react'; import { useMemo } from 'react';
import { import {
@ -13,8 +13,9 @@ import { clamp, find, isNumber, pull, remove, take, uniq } from 'lodash';
import { SortEnd } from 'react-sortable-hoc'; import { SortEnd } from 'react-sortable-hoc';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import arrayMove from 'array-move'; import arrayMove from 'array-move';
// eslint-disable-next-line import/no-cycle
import { AppState } from '../reducer'; import { AppState } from '../reducer';
import { PackMetaData, WebpData } from '../../util/preload'; import { PackMetaData, WebpData, StickerData } from '../../util/preload';
import { EmojiPickDataType } from '../../../ts/components/emoji/EmojiPicker'; import { EmojiPickDataType } from '../../../ts/components/emoji/EmojiPicker';
import { convertShortName } from '../../../ts/components/emoji/lib'; import { convertShortName } from '../../../ts/components/emoji/lib';
@ -46,6 +47,16 @@ export const minStickers = 1;
export const maxStickers = 200; export const maxStickers = 200;
export const maxByteSize = 100 * 1024; export const maxByteSize = 100 * 1024;
interface StateStickerData {
readonly webp?: WebpData;
readonly emoji?: EmojiPickDataType;
}
interface StateToastData {
key: string;
subs?: Array<number | string>;
}
export type State = { export type State = {
readonly order: Array<string>; readonly order: Array<string>;
readonly cover?: WebpData; readonly cover?: WebpData;
@ -53,15 +64,28 @@ export type State = {
readonly author: string; readonly author: string;
readonly packId: string; readonly packId: string;
readonly packKey: string; readonly packKey: string;
readonly toasts: Array<{ key: string; subs?: Array<number | string> }>; readonly toasts: Array<StateToastData>;
readonly data: { readonly data: {
readonly [src: string]: { readonly [src: string]: StateStickerData;
readonly webp?: WebpData;
readonly emoji?: EmojiPickDataType;
};
}; };
}; };
export type Actions = {
addWebp: typeof addWebp;
initializeStickers: typeof initializeStickers;
removeSticker: typeof removeSticker;
moveSticker: typeof moveSticker;
setCover: typeof setCover;
setEmoji: typeof setEmoji;
setTitle: typeof setTitle;
setAuthor: typeof setAuthor;
setPackMeta: typeof setPackMeta;
addToast: typeof addToast;
dismissToast: typeof dismissToast;
reset: typeof reset;
resetStatus: typeof resetStatus;
};
const defaultState: State = { const defaultState: State = {
order: [], order: [],
data: {}, data: {},
@ -193,21 +217,22 @@ export const reducer = reduceReducers<State>(
defaultState defaultState
); );
export const useTitle = () => export const useTitle = (): string =>
useSelector(({ stickers }: AppState) => stickers.title); useSelector(({ stickers }: AppState) => stickers.title);
export const useAuthor = () =>
export const useAuthor = (): string =>
useSelector(({ stickers }: AppState) => stickers.author); useSelector(({ stickers }: AppState) => stickers.author);
export const useCover = () => export const useCover = (): WebpData | undefined =>
useSelector(({ stickers }: AppState) => stickers.cover); useSelector(({ stickers }: AppState) => stickers.cover);
export const useStickerOrder = () => export const useStickerOrder = (): Array<string> =>
useSelector(({ stickers }: AppState) => stickers.order); useSelector(({ stickers }: AppState) => stickers.order);
export const useStickerData = (src: string) => export const useStickerData = (src: string): StateStickerData =>
useSelector(({ stickers }: AppState) => stickers.data[src]); useSelector(({ stickers }: AppState) => stickers.data[src]);
export const useStickersReady = () => export const useStickersReady = (): boolean =>
useSelector( useSelector(
({ stickers }: AppState) => ({ stickers }: AppState) =>
stickers.order.length >= minStickers && stickers.order.length >= minStickers &&
@ -215,12 +240,12 @@ export const useStickersReady = () =>
Object.values(stickers.data).every(({ webp }) => !!webp) Object.values(stickers.data).every(({ webp }) => !!webp)
); );
export const useEmojisReady = () => export const useEmojisReady = (): boolean =>
useSelector(({ stickers }: AppState) => useSelector(({ stickers }: AppState) =>
Object.values(stickers.data).every(({ emoji }) => !!emoji) Object.values(stickers.data).every(({ emoji }) => !!emoji)
); );
export const useAllDataValid = () => { export const useAllDataValid = (): boolean => {
const stickersReady = useStickersReady(); const stickersReady = useStickersReady();
const emojisReady = useEmojisReady(); const emojisReady = useEmojisReady();
const cover = useCover(); const cover = useCover();
@ -236,10 +261,12 @@ const selectUrl = createSelector(
(id, key) => `https://signal.art/addstickers/#pack_id=${id}&pack_key=${key}` (id, key) => `https://signal.art/addstickers/#pack_id=${id}&pack_key=${key}`
); );
export const usePackUrl = () => useSelector(selectUrl); export const usePackUrl = (): string => useSelector(selectUrl);
export const useToasts = () =>
export const useToasts = (): Array<StateToastData> =>
useSelector(({ stickers }: AppState) => stickers.toasts); useSelector(({ stickers }: AppState) => stickers.toasts);
export const useAddMoreCount = () =>
export const useAddMoreCount = (): number =>
useSelector(({ stickers }: AppState) => useSelector(({ stickers }: AppState) =>
clamp(minStickers - stickers.order.length, 0, minStickers) clamp(minStickers - stickers.order.length, 0, minStickers)
); );
@ -251,21 +278,23 @@ const selectOrderedData = createSelector(
order.map(id => ({ order.map(id => ({
...data[id], ...data[id],
emoji: convertShortName( emoji: convertShortName(
data[id].emoji.shortName, (data[id].emoji as EmojiPickDataType).shortName,
data[id].emoji.skinTone (data[id].emoji as EmojiPickDataType).skinTone
), ),
})) }))
); );
export const useSelectOrderedData = () => useSelector(selectOrderedData); export const useSelectOrderedData = (): Array<StickerData> =>
useSelector(selectOrderedData);
const selectOrderedImagePaths = createSelector(selectOrderedData, data => const selectOrderedImagePaths = createSelector(selectOrderedData, data =>
data.map(({ webp }) => webp.src) data.map(({ webp }) => (webp as WebpData).src)
); );
export const useOrderedImagePaths = () => useSelector(selectOrderedImagePaths); export const useOrderedImagePaths = (): Array<string> =>
useSelector(selectOrderedImagePaths);
export const useStickerActions = () => { export const useStickerActions = (): Actions => {
const dispatch = useDispatch(); const dispatch = useDispatch();
return useMemo( return useMemo(

View file

@ -1,4 +1,5 @@
import { combineReducers, Reducer } from 'redux'; import { combineReducers, Reducer } from 'redux';
// eslint-disable-next-line import/no-cycle
import { reducer as stickers } from './ducks/stickers'; import { reducer as stickers } from './ducks/stickers';
export const reducer = combineReducers({ export const reducer = combineReducers({

View file

@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"lib": ["dom", "es2017"],
"jsx": "react",
"rootDir": "."
}
}

View file

@ -16,7 +16,7 @@ export type I18nProps = {
messages: { [key: string]: { message: string } }; messages: { [key: string]: { message: string } };
}; };
export const I18n = ({ messages, children }: I18nProps) => { export const I18n = ({ messages, children }: I18nProps): JSX.Element => {
const getMessage = React.useCallback<I18nFn>( const getMessage = React.useCallback<I18nFn>(
(key, substitutions) => { (key, substitutions) => {
if (Array.isArray(substitutions) && substitutions.length > 1) { if (Array.isArray(substitutions) && substitutions.length > 1) {
@ -28,7 +28,8 @@ export const I18n = ({ messages, children }: I18nProps) => {
const { message } = messages[key]; const { message } = messages[key];
if (!substitutions) { if (!substitutions) {
return message; return message;
} else if (Array.isArray(substitutions)) { }
if (Array.isArray(substitutions)) {
return substitutions.reduce( return substitutions.reduce(
(result, substitution) => (result, substitution) =>
result.toString().replace(/\$.+?\$/, substitution.toString()), result.toString().replace(/\$.+?\$/, substitution.toString()),
@ -50,7 +51,7 @@ export const I18n = ({ messages, children }: I18nProps) => {
const placeholderName = match[1]; const placeholderName = match[1];
const value = substitutions[placeholderName]; const value = substitutions[placeholderName];
if (!value) { if (!value) {
// tslint:disable-next-line no-console // eslint-disable-next-line no-console
console.error( console.error(
`i18n: Value not provided for placeholder ${placeholderName} in key '${key}'` `i18n: Value not provided for placeholder ${placeholderName} in key '${key}'`
); );
@ -75,4 +76,4 @@ export const I18n = ({ messages, children }: I18nProps) => {
); );
}; };
export const useI18n = () => React.useContext(I18nContext); export const useI18n = (): I18nFn => React.useContext(I18nContext);

View file

@ -1,5 +1,12 @@
import { Metadata } from 'sharp'; import { Metadata } from 'sharp';
declare global {
interface Window {
convertToWebp: ConvertToWebpFn;
encryptAndUpload: EncryptAndUploadFn;
}
}
export type WebpData = { export type WebpData = {
buffer: Buffer; buffer: Buffer;
src: string; src: string;
@ -13,9 +20,6 @@ export type ConvertToWebpFn = (
height?: number height?: number
) => Promise<WebpData>; ) => Promise<WebpData>;
// @ts-ignore
export const convertToWebp: ConvertToWebpFn = window.convertToWebp;
export type StickerData = { webp?: WebpData; emoji?: string }; export type StickerData = { webp?: WebpData; emoji?: string };
export type PackMetaData = { packId: string; key: string }; export type PackMetaData = { packId: string; key: string };
@ -26,5 +30,4 @@ export type EncryptAndUploadFn = (
onProgress?: () => unknown onProgress?: () => unknown
) => Promise<PackMetaData>; ) => Promise<PackMetaData>;
// @ts-ignore export const { encryptAndUpload, convertToWebp } = window;
export const encryptAndUpload: EncryptAndUploadFn = window.encryptAndUpload;

View file

@ -182,7 +182,7 @@ export class MessageDetail extends React.Component<Props> {
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}> <div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
<div className="module-message-detail__message-container"> <div className="module-message-detail__message-container">
<Message i18n={i18n} {...message} /> <Message {...message} i18n={i18n} />
</div> </div>
<table className="module-message-detail__info"> <table className="module-message-detail__info">
<tbody> <tbody>

8
ts/model-types.d.ts vendored
View file

@ -18,7 +18,9 @@ type DeletesAttributesType = {
targetSentTimestamp: number; targetSentTimestamp: number;
}; };
declare class DeletesModelType extends Backbone.Model<DeletesAttributesType> { export declare class DeletesModelType extends Backbone.Model<
DeletesAttributesType
> {
forMessage(message: MessageModelType): Array<DeletesModelType>; forMessage(message: MessageModelType): Array<DeletesModelType>;
onDelete(doe: DeletesAttributesType): Promise<void>; onDelete(doe: DeletesAttributesType): Promise<void>;
} }
@ -47,7 +49,9 @@ export type MessageAttributesType = {
sourceUuid?: string; sourceUuid?: string;
}; };
declare class MessageModelType extends Backbone.Model<MessageAttributesType> { export declare class MessageModelType extends Backbone.Model<
MessageAttributesType
> {
id: string; id: string;
static updateTimers(): void; static updateTimers(): void;

View file

@ -1,16 +1,27 @@
import { applyMiddleware, createStore as reduxCreateStore } from 'redux'; /* eslint-disable no-console */
import {
applyMiddleware,
createStore as reduxCreateStore,
DeepPartial,
Store,
} from 'redux';
import promise from 'redux-promise-middleware'; import promise from 'redux-promise-middleware';
import { createLogger } from 'redux-logger'; import { createLogger } from 'redux-logger';
import { reducer } from './reducer'; import { reducer, StateType } from './reducer';
declare global {
interface Console {
_log: Console['log'];
}
}
// @ts-ignore
const env = window.getEnvironment(); const env = window.getEnvironment();
// So Redux logging doesn't go to disk, and so we can get colors/styles // So Redux logging doesn't go to disk, and so we can get colors/styles
const directConsole = { const directConsole = {
// @ts-ignore
log: console._log, log: console._log,
groupCollapsed: console.groupCollapsed, groupCollapsed: console.groupCollapsed,
group: console.group, group: console.group,
@ -27,7 +38,7 @@ const logger = createLogger({
// Exclude logger if we're in production mode // Exclude logger if we're in production mode
const middlewareList = env === 'production' ? [promise] : [promise, logger]; const middlewareList = env === 'production' ? [promise] : [promise, logger];
const enhancer = applyMiddleware.apply(null, middlewareList); const enhancer = applyMiddleware(...middlewareList);
export const createStore = (initialState: any) => export const createStore = (initialState: DeepPartial<StateType>): Store =>
reduxCreateStore(reducer, initialState, enhancer); reduxCreateStore(reducer, initialState, enhancer);

View file

@ -17,7 +17,7 @@ import {
// State // State
export type CallId = any; export type CallId = unknown;
export type CallDetailsType = { export type CallDetailsType = {
callId: CallId; callId: CallId;

View file

@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import { import {
difference, difference,
fromPairs, fromPairs,
@ -113,7 +114,7 @@ export type MessageType = {
deletedForEveryone?: boolean; deletedForEveryone?: boolean;
errors?: Array<Error>; errors?: Array<Error>;
group_update?: any; group_update?: unknown;
// No need to go beyond this; unused at this stage, since this goes into // No need to go beyond this; unused at this stage, since this goes into
// a reducer still in plain JavaScript and comes out well-formed // a reducer still in plain JavaScript and comes out well-formed
@ -393,7 +394,10 @@ function removeAllConversations(): RemoveAllConversationsActionType {
}; };
} }
function selectMessage(messageId: string, conversationId: string) { function selectMessage(
messageId: string,
conversationId: string
): MessageSelectedActionType {
return { return {
type: 'MESSAGE_SELECTED', type: 'MESSAGE_SELECTED',
payload: { payload: {
@ -567,13 +571,13 @@ function openConversationExternal(
}; };
} }
function showInbox() { function showInbox(): ShowInboxActionType {
return { return {
type: 'SHOW_INBOX', type: 'SHOW_INBOX',
payload: null, payload: null,
}; };
} }
function showArchivedConversations() { function showArchivedConversations(): ShowArchivedConversationsActionType {
return { return {
type: 'SHOW_ARCHIVED_CONVERSATIONS', type: 'SHOW_ARCHIVED_CONVERSATIONS',
payload: null, payload: null,
@ -596,7 +600,7 @@ function getEmptyState(): ConversationsStateType {
function hasMessageHeightChanged( function hasMessageHeightChanged(
message: MessageType, message: MessageType,
previous: MessageType previous: MessageType
): Boolean { ): boolean {
const messageAttachments = message.attachments || []; const messageAttachments = message.attachments || [];
const previousAttachments = previous.attachments || []; const previousAttachments = previous.attachments || [];
@ -687,8 +691,7 @@ export function reducer(
const { id, data } = payload; const { id, data } = payload;
const { conversationLookup } = state; const { conversationLookup } = state;
let showArchived = state.showArchived; let { showArchived, selectedConversation } = state;
let selectedConversation = state.selectedConversation;
const existing = conversationLookup[id]; const existing = conversationLookup[id];
// In the change case we only modify the lookup if we already had that conversation // In the change case we only modify the lookup if we already had that conversation
@ -1087,7 +1090,8 @@ export function reducer(
} }
} }
// Update oldest and newest if we receive older/newer messages (or duplicated timestamps!) // Update oldest and newest if we receive older/newer
// messages (or duplicated timestamps!)
if (first && oldest && first.received_at <= oldest.received_at) { if (first && oldest && first.received_at <= oldest.received_at) {
oldest = pick(first, ['id', 'received_at']); oldest = pick(first, ['id', 'received_at']);
} }

View file

@ -31,7 +31,7 @@ export const actions = {
onUseEmoji, onUseEmoji,
}; };
export const useActions = () => useBoundActions(actions); export const useActions = (): typeof actions => useBoundActions(actions);
function onUseEmoji({ shortName }: EmojiPickDataType): OnUseEmojiAction { function onUseEmoji({ shortName }: EmojiPickDataType): OnUseEmojiAction {
return { return {

View file

@ -9,7 +9,7 @@ import { useBoundActions } from '../../util/hooks';
// State // State
export type ItemsStateType = { export type ItemsStateType = {
readonly [key: string]: any; readonly [key: string]: unknown;
}; };
// Actions // Actions
@ -23,7 +23,7 @@ type ItemPutExternalAction = {
type: 'items/PUT_EXTERNAL'; type: 'items/PUT_EXTERNAL';
payload: { payload: {
key: string; key: string;
value: any; value: unknown;
}; };
}; };
@ -58,9 +58,9 @@ export const actions = {
resetItems, resetItems,
}; };
export const useActions = () => useBoundActions(actions); export const useActions = (): typeof actions => useBoundActions(actions);
function putItem(key: string, value: any): ItemPutAction { function putItem(key: string, value: unknown): ItemPutAction {
storageShim.put(key, value); storageShim.put(key, value);
return { return {
@ -69,7 +69,7 @@ function putItem(key: string, value: any): ItemPutAction {
}; };
} }
function putItemExternal(key: string, value: any): ItemPutExternalAction { function putItemExternal(key: string, value: unknown): ItemPutExternalAction {
return { return {
type: 'items/PUT_EXTERNAL', type: 'items/PUT_EXTERNAL',
payload: { payload: {
@ -139,4 +139,5 @@ const selectRecentEmojis = createSelector(
recents => recents.filter(isShortName) recents => recents.filter(isShortName)
); );
export const useRecentEmojis = () => useSelector(selectRecentEmojis); export const useRecentEmojis = (): Array<string> =>
useSelector(selectRecentEmojis);

View file

@ -292,7 +292,7 @@ async function queryConversationsAndContacts(
for (let i = 0; i < max; i += 1) { for (let i = 0; i < max; i += 1) {
const conversation = searchResults[i]; const conversation = searchResults[i];
if (conversation.type === 'private' && !Boolean(conversation.lastMessage)) { if (conversation.type === 'private' && !conversation.lastMessage) {
contacts.push(conversation.id); contacts.push(conversation.id);
} else { } else {
conversations.push(conversation.id); conversations.push(conversation.id);

View file

@ -376,7 +376,10 @@ export function reducer(
action: StickersActionType action: StickersActionType
): StickersStateType { ): StickersStateType {
if (action.type === 'stickers/STICKER_PACK_ADDED') { if (action.type === 'stickers/STICKER_PACK_ADDED') {
const { payload } = action; // ts complains due to `stickers: {}` being overridden by the payload
// but without full confidence that that's the case, `any` and ignore
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { payload } = action as any;
const newPack = { const newPack = {
stickers: {}, stickers: {},
...payload, ...payload,

View file

@ -92,5 +92,5 @@ export const reducers = {
user, user,
}; };
// @ts-ignore: AnyAction breaks strong type checking inside reducers // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const reducer = combineReducers<StateType, ActionsType>(reducers); export const reducer = combineReducers<StateType, ActionsType>(reducers as any);

View file

@ -7,9 +7,11 @@ import { SmartCallManager } from '../smart/CallManager';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredCallManager = SmartCallManager as any; const FilteredCallManager = SmartCallManager as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const createCallManager = (store: Store) => ( export const createCallManager = (store: Store): React.ReactElement => (
<Provider store={store}> <Provider store={store}>
<FilteredCallManager /> <FilteredCallManager />
</Provider> </Provider>

View file

@ -7,9 +7,14 @@ import { SmartCompositionArea } from '../smart/CompositionArea';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredCompositionArea = SmartCompositionArea as any; const FilteredCompositionArea = SmartCompositionArea as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const createCompositionArea = (store: Store, props: Object) => ( export const createCompositionArea = (
store: Store,
props: Record<string, unknown>
): React.ReactElement => (
<Provider store={store}> <Provider store={store}>
<FilteredCompositionArea {...props} /> <FilteredCompositionArea {...props} />
</Provider> </Provider>

View file

@ -7,9 +7,11 @@ import { SmartLeftPane } from '../smart/LeftPane';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredLeftPane = SmartLeftPane as any; const FilteredLeftPane = SmartLeftPane as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const createLeftPane = (store: Store) => ( export const createLeftPane = (store: Store): React.ReactElement => (
<Provider store={store}> <Provider store={store}>
<FilteredLeftPane /> <FilteredLeftPane />
</Provider> </Provider>

View file

@ -7,14 +7,19 @@ import { SmartSafetyNumberViewer } from '../smart/SafetyNumberViewer';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredSafetyNumberViewer = SmartSafetyNumberViewer as any; const FilteredSafetyNumberViewer = SmartSafetyNumberViewer as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
type Props = { type Props = {
contactID: string; contactID: string;
onClose?: () => void; onClose?: () => void;
}; };
export const createSafetyNumberViewer = (store: Store, props: Props) => ( export const createSafetyNumberViewer = (
store: Store,
props: Props
): React.ReactElement => (
<Provider store={store}> <Provider store={store}>
<FilteredSafetyNumberViewer {...props} /> <FilteredSafetyNumberViewer {...props} />
</Provider> </Provider>

View file

@ -7,9 +7,14 @@ import { SmartShortcutGuideModal } from '../smart/ShortcutGuideModal';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredShortcutGuideModal = SmartShortcutGuideModal as any; const FilteredShortcutGuideModal = SmartShortcutGuideModal as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const createShortcutGuideModal = (store: Store, props: Object) => ( export const createShortcutGuideModal = (
store: Store,
props: Record<string, unknown>
): React.ReactElement => (
<Provider store={store}> <Provider store={store}>
<FilteredShortcutGuideModal {...props} /> <FilteredShortcutGuideModal {...props} />
</Provider> </Provider>

View file

@ -7,9 +7,11 @@ import { SmartStickerManager } from '../smart/StickerManager';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredStickerManager = SmartStickerManager as any; const FilteredStickerManager = SmartStickerManager as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const createStickerManager = (store: Store) => ( export const createStickerManager = (store: Store): React.ReactElement => (
<Provider store={store}> <Provider store={store}>
<FilteredStickerManager /> <FilteredStickerManager />
</Provider> </Provider>

View file

@ -7,9 +7,14 @@ import { SmartStickerPreviewModal } from '../smart/StickerPreviewModal';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredStickerPreviewModal = SmartStickerPreviewModal as any; const FilteredStickerPreviewModal = SmartStickerPreviewModal as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const createStickerPreviewModal = (store: Store, props: Object) => ( export const createStickerPreviewModal = (
store: Store,
props: Record<string, unknown>
): React.ReactElement => (
<Provider store={store}> <Provider store={store}>
<FilteredStickerPreviewModal {...props} /> <FilteredStickerPreviewModal {...props} />
</Provider> </Provider>

View file

@ -7,9 +7,14 @@ import { SmartTimeline } from '../smart/Timeline';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredTimeline = SmartTimeline as any; const FilteredTimeline = SmartTimeline as any;
/* eslint-disable @typescript-eslint/no-explicit-any */
export const createTimeline = (store: Store, props: Object) => ( export const createTimeline = (
store: Store,
props: Record<string, unknown>
): React.ReactElement => (
<Provider store={store}> <Provider store={store}>
<FilteredTimeline {...props} /> <FilteredTimeline {...props} />
</Provider> </Provider>

View file

@ -49,7 +49,7 @@ export const getSelectedMessage = createSelector(
getConversations, getConversations,
(state: ConversationsStateType): SelectedMessageType | undefined => { (state: ConversationsStateType): SelectedMessageType | undefined => {
if (!state.selectedMessage) { if (!state.selectedMessage) {
return; return undefined;
} }
return { return {
@ -136,21 +136,19 @@ export const _getLeftPaneLists = (
const max = values.length; const max = values.length;
for (let i = 0; i < max; i += 1) { for (let i = 0; i < max; i += 1) {
let conversation = values[i]; let conversation = values[i];
if (!conversation.activeAt) { if (conversation.activeAt) {
continue; if (selectedConversation === conversation.id) {
} conversation = {
...conversation,
isSelected: true,
};
}
if (selectedConversation === conversation.id) { if (conversation.isArchived) {
conversation = { archivedConversations.push(conversation);
...conversation, } else {
isSelected: true, conversations.push(conversation);
}; }
}
if (conversation.isArchived) {
archivedConversations.push(conversation);
} else {
conversations.push(conversation);
} }
} }
@ -220,7 +218,7 @@ export const getConversationSelector = createSelector(
return (id: string) => { return (id: string) => {
const conversation = lookup[id]; const conversation = lookup[id];
if (!conversation) { if (!conversation) {
return; return undefined;
} }
return selector(conversation); return selector(conversation);
@ -236,17 +234,12 @@ export const getConversationSelector = createSelector(
// - message details // - message details
export function _messageSelector( export function _messageSelector(
message: MessageType, message: MessageType,
// @ts-ignore _ourNumber: string,
ourNumber: string, _regionCode: string,
// @ts-ignore
regionCode: string,
interactionMode: 'mouse' | 'keyboard', interactionMode: 'mouse' | 'keyboard',
// @ts-ignore _conversation?: ConversationType,
conversation?: ConversationType, _author?: ConversationType,
// @ts-ignore _quoted?: ConversationType,
author?: ConversationType,
// @ts-ignore
quoted?: ConversationType,
selectedMessageId?: string, selectedMessageId?: string,
selectedMessageCounter?: number selectedMessageCounter?: number
): TimelineItemType { ): TimelineItemType {
@ -319,7 +312,7 @@ export const getMessageSelector = createSelector(
return (id: string) => { return (id: string) => {
const message = messageLookup[id]; const message = messageLookup[id];
if (!message) { if (!message) {
return; return undefined;
} }
const { conversationId, source, type, quote } = message; const { conversationId, source, type, quote } = message;
@ -441,7 +434,7 @@ export const getConversationMessagesSelector = createSelector(
return (id: string): TimelinePropsType | undefined => { return (id: string): TimelinePropsType | undefined => {
const conversation = messagesByConversation[id]; const conversation = messagesByConversation[id];
if (!conversation) { if (!conversation) {
return; return undefined;
} }
return conversationMessagesSelector(conversation); return conversationMessagesSelector(conversation);

View file

@ -207,7 +207,7 @@ export const getSearchResults = createSelector(
items, items,
messagesLoading, messagesLoading,
noResults, noResults,
regionCode: regionCode, regionCode,
searchConversationName, searchConversationName,
searchTerm: state.query, searchTerm: state.query,
selectedConversationId, selectedConversationId,
@ -218,14 +218,10 @@ export const getSearchResults = createSelector(
export function _messageSearchResultSelector( export function _messageSearchResultSelector(
message: MessageSearchResultType, message: MessageSearchResultType,
// @ts-ignore _ourNumber: string,
ourNumber: string, _regionCode: string,
// @ts-ignore _sender?: ConversationType,
regionCode: string, _recipient?: ConversationType,
// @ts-ignore
sender?: ConversationType,
// @ts-ignore
recipient?: ConversationType,
searchConversationId?: string, searchConversationId?: string,
selectedMessageId?: string selectedMessageId?: string
): MessageSearchResultPropsDataType { ): MessageSearchResultPropsDataType {
@ -282,7 +278,7 @@ export const getMessageSearchResultSelector = createSelector(
return (id: string) => { return (id: string) => {
const message = messageSearchResultLookup[id]; const message = messageSearchResultLookup[id];
if (!message) { if (!message) {
return; return undefined;
} }
const { conversationId, source, type } = message; const { conversationId, source, type } = message;

View file

@ -31,12 +31,12 @@ const getSticker = (
): StickerType | undefined => { ): StickerType | undefined => {
const pack = packs[packId]; const pack = packs[packId];
if (!pack) { if (!pack) {
return; return undefined;
} }
const sticker = pack.stickers[stickerId]; const sticker = pack.stickers[stickerId];
if (!sticker) { if (!sticker) {
return; return undefined;
} }
const isEphemeral = pack.status === 'ephemeral'; const isEphemeral = pack.status === 'ephemeral';
@ -67,7 +67,7 @@ export const translatePackFromDB = (
blessedPacks: Dictionary<boolean>, blessedPacks: Dictionary<boolean>,
stickersPath: string, stickersPath: string,
tempPath: string tempPath: string
) => { ): StickerPackType => {
const { id, stickers, status, coverStickerId } = pack; const { id, stickers, status, coverStickerId } = pack;
const isEphemeral = status === 'ephemeral'; const isEphemeral = status === 'ephemeral';
@ -92,7 +92,7 @@ export const translatePackFromDB = (
const filterAndTransformPacks = ( const filterAndTransformPacks = (
packs: Dictionary<StickerPackDBType>, packs: Dictionary<StickerPackDBType>,
packFilter: (sticker: StickerPackDBType) => boolean, packFilter: (sticker: StickerPackDBType) => boolean,
packSort: (sticker: StickerPackDBType) => any, packSort: (sticker: StickerPackDBType) => number | null,
blessedPacks: Dictionary<boolean>, blessedPacks: Dictionary<boolean>,
stickersPath: string, stickersPath: string,
tempPath: string tempPath: string

View file

@ -32,7 +32,7 @@ export const getUserUuid = createSelector(
export const getUserAgent = createSelector( export const getUserAgent = createSelector(
getItems, getItems,
(state: ItemsStateType): string => state.userAgent (state: ItemsStateType): string => state.userAgent as string
); );
export const getIntl = createSelector( export const getIntl = createSelector(

View file

@ -10,7 +10,9 @@ import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredCallingDeviceSelection = SmartCallingDeviceSelection as any; const FilteredCallingDeviceSelection = SmartCallingDeviceSelection as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
function renderDeviceSelection(): JSX.Element { function renderDeviceSelection(): JSX.Element {
return <FilteredCallingDeviceSelection />; return <FilteredCallingDeviceSelection />;

View file

@ -89,4 +89,5 @@ const dispatchPropsMap = {
const smart = connect(mapStateToProps, dispatchPropsMap); const smart = connect(mapStateToProps, dispatchPropsMap);
export const SmartCompositionArea = smart(CompositionArea); // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const SmartCompositionArea = smart(CompositionArea as any);

View file

@ -16,7 +16,7 @@ type ExternalProps = {
conversationId: string; conversationId: string;
}; };
export const SmartContactName = (props: ExternalProps) => { export const SmartContactName: React.ComponentType<ExternalProps> = props => {
const { conversationId } = props; const { conversationId } = props;
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector<StateType, LocalizerType>(getIntl);
const getConversation = useSelector<StateType, GetConversationByIdType>( const getConversation = useSelector<StateType, GetConversationByIdType>(

View file

@ -21,12 +21,14 @@ import { SmartUpdateDialog } from './UpdateDialog';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredSmartMainHeader = SmartMainHeader as any; const FilteredSmartMainHeader = SmartMainHeader as any;
const FilteredSmartMessageSearchResult = SmartMessageSearchResult as any; const FilteredSmartMessageSearchResult = SmartMessageSearchResult as any;
const FilteredSmartNetworkStatus = SmartNetworkStatus as any; const FilteredSmartNetworkStatus = SmartNetworkStatus as any;
const FilteredSmartUpdateDialog = SmartUpdateDialog as any; const FilteredSmartUpdateDialog = SmartUpdateDialog as any;
const FilteredSmartExpiredBuildDialog = SmartExpiredBuildDialog as any; const FilteredSmartExpiredBuildDialog = SmartExpiredBuildDialog as any;
const FilteredSmartRelinkDialog = SmartRelinkDialog as any; const FilteredSmartRelinkDialog = SmartRelinkDialog as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
function renderExpiredBuildDialog(): JSX.Element { function renderExpiredBuildDialog(): JSX.Element {
return <FilteredSmartExpiredBuildDialog />; return <FilteredSmartExpiredBuildDialog />;

View file

@ -22,11 +22,13 @@ import { SmartEmojiPicker } from './EmojiPicker';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredSmartTimelineItem = SmartTimelineItem as any; const FilteredSmartTimelineItem = SmartTimelineItem as any;
const FilteredSmartTypingBubble = SmartTypingBubble as any; const FilteredSmartTypingBubble = SmartTypingBubble as any;
const FilteredSmartLastSeenIndicator = SmartLastSeenIndicator as any; const FilteredSmartLastSeenIndicator = SmartLastSeenIndicator as any;
const FilteredSmartHeroRow = SmartHeroRow as any; const FilteredSmartHeroRow = SmartHeroRow as any;
const FilteredSmartTimelineLoadingRow = SmartTimelineLoadingRow as any; const FilteredSmartTimelineLoadingRow = SmartTimelineLoadingRow as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
type ExternalProps = { type ExternalProps = {
id: string; id: string;
@ -38,7 +40,7 @@ type ExternalProps = {
function renderItem( function renderItem(
messageId: string, messageId: string,
conversationId: string, conversationId: string,
actionProps: Object actionProps: Record<string, unknown>
): JSX.Element { ): JSX.Element {
return ( return (
<FilteredSmartTimelineItem <FilteredSmartTimelineItem
@ -61,7 +63,7 @@ function renderEmojiPicker({
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
onClose={onClose} onClose={onClose}
style={style} style={style}
disableSkinTones={true} disableSkinTones
/> />
); );
} }
@ -112,4 +114,5 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const smart = connect(mapStateToProps, mapDispatchToProps); const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartTimeline = smart(Timeline); // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const SmartTimeline = smart(Timeline as any);

View file

@ -20,7 +20,9 @@ type ExternalProps = {
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredSmartContactName = SmartContactName as any; const FilteredSmartContactName = SmartContactName as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
function renderContact(conversationId: string): JSX.Element { function renderContact(conversationId: string): JSX.Element {
return <FilteredSmartContactName conversationId={conversationId} />; return <FilteredSmartContactName conversationId={conversationId} />;

View file

@ -1,6 +1,6 @@
import { isNumber } from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
import { isNumber } from 'lodash';
import { import {
STATE_ENUM, STATE_ENUM,
@ -26,11 +26,16 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { isLoadingMessages, loadCountdownStart } = conversation; const { isLoadingMessages, loadCountdownStart } = conversation;
const loadingState: STATE_ENUM = isLoadingMessages let loadingState: STATE_ENUM;
? 'loading'
: isNumber(loadCountdownStart) if (isLoadingMessages) {
? 'countdown' loadingState = 'loading';
: 'idle'; } else if (isNumber(loadCountdownStart)) {
loadingState = 'countdown';
} else {
loadingState = 'idle';
}
const duration = loadingState === 'countdown' ? LOAD_COUNTDOWN : undefined; const duration = loadingState === 'countdown' ? LOAD_COUNTDOWN : undefined;
const expiresAt = const expiresAt =
loadingState === 'countdown' && loadCountdownStart loadingState === 'countdown' && loadCountdownStart

View file

@ -64,7 +64,7 @@ export function getExtensionForDisplay({
} }
if (!contentType) { if (!contentType) {
return; return undefined;
} }
const slash = contentType.indexOf('/'); const slash = contentType.indexOf('/');
@ -72,10 +72,12 @@ export function getExtensionForDisplay({
return contentType.slice(slash + 1); return contentType.slice(slash + 1);
} }
return; return undefined;
} }
export function isAudio(attachments?: Array<AttachmentType>) { export function isAudio(
attachments?: Array<AttachmentType>
): boolean | undefined {
return ( return (
attachments && attachments &&
attachments[0] && attachments[0] &&
@ -84,7 +86,9 @@ export function isAudio(attachments?: Array<AttachmentType>) {
); );
} }
export function canDisplayImage(attachments?: Array<AttachmentType>) { export function canDisplayImage(
attachments?: Array<AttachmentType>
): boolean | 0 | undefined {
const { height, width } = const { height, width } =
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 }; attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
@ -98,7 +102,7 @@ export function canDisplayImage(attachments?: Array<AttachmentType>) {
); );
} }
export function getThumbnailUrl(attachment: AttachmentType) { export function getThumbnailUrl(attachment: AttachmentType): string {
if (attachment.thumbnail) { if (attachment.thumbnail) {
return attachment.thumbnail.url; return attachment.thumbnail.url;
} }
@ -106,7 +110,7 @@ export function getThumbnailUrl(attachment: AttachmentType) {
return getUrl(attachment); return getUrl(attachment);
} }
export function getUrl(attachment: AttachmentType) { export function getUrl(attachment: AttachmentType): string {
if (attachment.screenshot) { if (attachment.screenshot) {
return attachment.screenshot.url; return attachment.screenshot.url;
} }
@ -114,7 +118,9 @@ export function getUrl(attachment: AttachmentType) {
return attachment.url; return attachment.url;
} }
export function isImage(attachments?: Array<AttachmentType>) { export function isImage(
attachments?: Array<AttachmentType>
): boolean | undefined {
return ( return (
attachments && attachments &&
attachments[0] && attachments[0] &&
@ -123,14 +129,18 @@ export function isImage(attachments?: Array<AttachmentType>) {
); );
} }
export function isImageAttachment(attachment: AttachmentType) { export function isImageAttachment(
attachment: AttachmentType
): boolean | undefined {
return ( return (
attachment && attachment &&
attachment.contentType && attachment.contentType &&
isImageTypeSupported(attachment.contentType) isImageTypeSupported(attachment.contentType)
); );
} }
export function hasImage(attachments?: Array<AttachmentType>) { export function hasImage(
attachments?: Array<AttachmentType>
): string | boolean | undefined {
return ( return (
attachments && attachments &&
attachments[0] && attachments[0] &&
@ -138,11 +148,15 @@ export function hasImage(attachments?: Array<AttachmentType>) {
); );
} }
export function isVideo(attachments?: Array<AttachmentType>) { export function isVideo(
attachments?: Array<AttachmentType>
): boolean | undefined {
return attachments && isVideoAttachment(attachments[0]); return attachments && isVideoAttachment(attachments[0]);
} }
export function isVideoAttachment(attachment?: AttachmentType) { export function isVideoAttachment(
attachment?: AttachmentType
): boolean | undefined {
return ( return (
attachment && attachment &&
attachment.contentType && attachment.contentType &&
@ -150,7 +164,9 @@ export function isVideoAttachment(attachment?: AttachmentType) {
); );
} }
export function hasVideoScreenshot(attachments?: Array<AttachmentType>) { export function hasVideoScreenshot(
attachments?: Array<AttachmentType>
): string | null | undefined {
const firstAttachment = attachments ? attachments[0] : null; const firstAttachment = attachments ? attachments[0] : null;
return ( return (
@ -308,7 +324,7 @@ export const isFile = (attachment: Attachment): boolean => {
export const isVoiceMessage = (attachment: Attachment): boolean => { export const isVoiceMessage = (attachment: Attachment): boolean => {
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE; const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
const hasFlag = const hasFlag =
// tslint:disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
!is.undefined(attachment.flags) && (attachment.flags & flag) === flag; !is.undefined(attachment.flags) && (attachment.flags & flag) === flag;
if (hasFlag) { if (hasFlag) {
return true; return true;
@ -390,7 +406,7 @@ export const getFileExtension = (
attachment: Attachment attachment: Attachment
): string | undefined => { ): string | undefined => {
if (!attachment.contentType) { if (!attachment.contentType) {
return; return undefined;
} }
switch (attachment.contentType) { switch (attachment.contentType) {

View file

@ -1,6 +1,4 @@
// @ts-ignore import { format as formatPhoneNumber } from './PhoneNumber';
import Attachments from '../../app/attachments';
import { format as formatPhoneNumber } from '../types/PhoneNumber';
export interface ContactType { export interface ContactType {
name?: Name; name?: Name;
@ -76,7 +74,7 @@ export function contactSelector(
signalAccount?: string; signalAccount?: string;
getAbsoluteAttachmentPath: (path: string) => string; getAbsoluteAttachmentPath: (path: string) => string;
} }
) { ): ContactType {
const { getAbsoluteAttachmentPath, signalAccount, regionCode } = options; const { getAbsoluteAttachmentPath, signalAccount, regionCode } = options;
let { avatar } = contact; let { avatar } = contact;

View file

@ -1,3 +1,5 @@
/* eslint-disable camelcase */
export enum Dialogs { export enum Dialogs {
None, None,
Update, Update,

View file

@ -1,4 +1,4 @@
type LogFunction = (...args: Array<any>) => void; type LogFunction = (...args: Array<unknown>) => void;
export type LoggerType = { export type LoggerType = {
fatal: LogFunction; fatal: LogFunction;

View file

@ -1,4 +1,4 @@
export type MIMEType = string & { _mimeTypeBrand: any }; export type MIMEType = string & { _mimeTypeBrand: never };
export const APPLICATION_OCTET_STREAM = 'application/octet-stream' as MIMEType; export const APPLICATION_OCTET_STREAM = 'application/octet-stream' as MIMEType;
export const APPLICATION_JSON = 'application/json' as MIMEType; export const APPLICATION_JSON = 'application/json' as MIMEType;

View file

@ -1,3 +1,5 @@
/* eslint-disable camelcase */
import { Attachment } from './Attachment'; import { Attachment } from './Attachment';
import { ContactType } from './Contact'; import { ContactType } from './Contact';
import { IndexableBoolean, IndexablePresence } from './IndexedDB'; import { IndexableBoolean, IndexablePresence } from './IndexedDB';
@ -21,7 +23,7 @@ export type IncomingMessage = Readonly<
// Optional // Optional
body?: string; body?: string;
decrypted_at?: number; decrypted_at?: number;
errors?: Array<any>; errors?: Array<Error>;
expireTimer?: number; expireTimer?: number;
messageTimer?: number; // deprecated messageTimer?: number; // deprecated
isViewOnce?: number; isViewOnce?: number;

View file

@ -1,5 +1,5 @@
import { instance, PhoneNumberFormat } from '../util/libphonenumberInstance';
import memoizee from 'memoizee'; import memoizee from 'memoizee';
import { instance, PhoneNumberFormat } from '../util/libphonenumberInstance';
function _format( function _format(
phoneNumber: string, phoneNumber: string,
@ -73,8 +73,8 @@ export function normalize(
return instance.format(parsedNumber, PhoneNumberFormat.E164); return instance.format(parsedNumber, PhoneNumberFormat.E164);
} }
return; return undefined;
} catch (error) { } catch (error) {
return; return undefined;
} }
} }

View file

@ -11,7 +11,8 @@ export enum AudioNotificationSupport {
export function getAudioNotificationSupport(): AudioNotificationSupport { export function getAudioNotificationSupport(): AudioNotificationSupport {
if (OS.isWindows(MIN_WINDOWS_VERSION) || OS.isMacOS()) { if (OS.isWindows(MIN_WINDOWS_VERSION) || OS.isMacOS()) {
return AudioNotificationSupport.Native; return AudioNotificationSupport.Native;
} else if (OS.isLinux()) { }
if (OS.isLinux()) {
return AudioNotificationSupport.Custom; return AudioNotificationSupport.Custom;
} }
return AudioNotificationSupport.None; return AudioNotificationSupport.None;
@ -22,11 +23,11 @@ export const isAudioNotificationSupported = (): boolean =>
// Using `Notification::tag` has a bug on Windows 7: // Using `Notification::tag` has a bug on Windows 7:
// https://github.com/electron/electron/issues/11189 // https://github.com/electron/electron/issues/11189
export const isNotificationGroupingSupported = () => export const isNotificationGroupingSupported = (): boolean =>
!OS.isWindows() || OS.isWindows(MIN_WINDOWS_VERSION); !OS.isWindows() || OS.isWindows(MIN_WINDOWS_VERSION);
// the "hide menu bar" option is specific to Windows and Linux // the "hide menu bar" option is specific to Windows and Linux
export const isHideMenuBarSupported = () => !OS.isMacOS(); export const isHideMenuBarSupported = (): boolean => !OS.isMacOS();
// the "draw attention on notification" option is specific to Windows and Linux // the "draw attention on notification" option is specific to Windows and Linux
export const isDrawAttentionSupported = () => !OS.isMacOS(); export const isDrawAttentionSupported = (): boolean => !OS.isMacOS();

View file

@ -7,8 +7,11 @@ export class Sound {
static sounds = new Map(); static sounds = new Map();
private readonly context = new AudioContext(); private readonly context = new AudioContext();
private readonly loop: boolean; private readonly loop: boolean;
private node?: AudioBufferSourceNode; private node?: AudioBufferSourceNode;
private readonly src: string; private readonly src: string;
constructor(options: SoundOpts) { constructor(options: SoundOpts) {
@ -16,7 +19,7 @@ export class Sound {
this.src = options.src; this.src = options.src;
} }
async play() { async play(): Promise<void> {
if (!Sound.sounds.has(this.src)) { if (!Sound.sounds.has(this.src)) {
try { try {
const buffer = await Sound.loadSoundFile(this.src); const buffer = await Sound.loadSoundFile(this.src);
@ -44,7 +47,7 @@ export class Sound {
this.node = soundNode; this.node = soundNode;
} }
stop() { stop(): void {
if (this.node) { if (this.node) {
this.node.stop(0); this.node.stop(0);
this.node = undefined; this.node = undefined;

View file

@ -1,11 +1,16 @@
import PQueue from 'p-queue'; import PQueue from 'p-queue';
// @ts-ignore declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
batchers: Array<BatcherType<any>>;
waitForAllBatchers: () => Promise<unknown>;
}
}
window.batchers = []; window.batchers = [];
// @ts-ignore
window.waitForAllBatchers = async () => { window.waitForAllBatchers = async () => {
// @ts-ignore
await Promise.all(window.batchers.map(item => item.flushAndWait())); await Promise.all(window.batchers.map(item => item.flushAndWait()));
}; };
@ -32,7 +37,7 @@ export function createBatcher<ItemType>(
options: BatcherOptionsType<ItemType> options: BatcherOptionsType<ItemType>
): BatcherType<ItemType> { ): BatcherType<ItemType> {
let batcher: BatcherType<ItemType>; let batcher: BatcherType<ItemType>;
let timeout: any; let timeout: NodeJS.Timeout | null;
let items: Array<ItemType> = []; let items: Array<ItemType> = [];
const queue = new PQueue({ concurrency: 1 }); const queue = new PQueue({ concurrency: 1 });
@ -70,18 +75,19 @@ export function createBatcher<ItemType>(
async function onIdle() { async function onIdle() {
while (anyPending()) { while (anyPending()) {
if (queue.size > 0 || queue.pending > 0) { if (queue.size > 0 || queue.pending > 0) {
// eslint-disable-next-line no-await-in-loop
await queue.onIdle(); await queue.onIdle();
} }
if (items.length > 0) { if (items.length > 0) {
// eslint-disable-next-line no-await-in-loop
await sleep(options.wait * 2); await sleep(options.wait * 2);
} }
} }
} }
function unregister() { function unregister() {
// @ts-ignore window.batchers = window.batchers.filter(item => item !== batcher);
window.batchers = window.batchers.filter((item: any) => item !== batcher);
} }
async function flushAndWait() { async function flushAndWait() {
@ -104,7 +110,6 @@ export function createBatcher<ItemType>(
unregister, unregister,
}; };
// @ts-ignore
window.batchers.push(batcher); window.batchers.push(batcher);
return batcher; return batcher;

View file

@ -1,12 +1,13 @@
import { Sound } from './Sound';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { Sound } from './Sound';
const ringtoneEventQueue = new PQueue({ concurrency: 1 }); const ringtoneEventQueue = new PQueue({ concurrency: 1 });
class CallingTones { class CallingTones {
private ringtone?: Sound; private ringtone?: Sound;
async playEndCall() { // eslint-disable-next-line class-methods-use-this
async playEndCall(): Promise<void> {
const canPlayTone = await window.getCallRingtoneNotification(); const canPlayTone = await window.getCallRingtoneNotification();
if (!canPlayTone) { if (!canPlayTone) {
return; return;

View file

@ -1,4 +1,4 @@
export function cleanSearchTerm(searchTerm: string) { export function cleanSearchTerm(searchTerm: string): string {
const lowercase = searchTerm.toLowerCase(); const lowercase = searchTerm.toLowerCase();
const withoutSpecialCharacters = lowercase.replace( const withoutSpecialCharacters = lowercase.replace(
/([!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~])/g, /([!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~])/g,

View file

@ -1,3 +1,5 @@
/* eslint-disable camelcase */
// We don't include unicode-12.1.0 because it's over 100MB in size // We don't include unicode-12.1.0 because it's over 100MB in size
// From https://github.com/mathiasbynens/unicode-12.1.0/tree/master/Block // From https://github.com/mathiasbynens/unicode-12.1.0/tree/master/Block
@ -50,6 +52,7 @@ export function combineNames(given: string, family?: string): null | string {
} }
function isAllCKJV(name: string): boolean { function isAllCKJV(name: string): boolean {
// eslint-disable-next-line no-restricted-syntax
for (const codePoint of name) { for (const codePoint of name) {
if (!isCKJV(codePoint)) { if (!isCKJV(codePoint)) {
return false; return false;
@ -59,7 +62,6 @@ function isAllCKJV(name: string): boolean {
return true; return true;
} }
// tslint:disable-next-line cyclomatic-complexity
function isCKJV(codePoint: string) { function isCKJV(codePoint: string) {
if (codePoint === ' ') { if (codePoint === ' ') {
return true; return true;

View file

@ -5,7 +5,7 @@ const ONE_DAY = 24 * 60 * 60 * 1000;
export async function deleteForEveryone( export async function deleteForEveryone(
message: MessageModelType, message: MessageModelType,
doe: DeletesModelType, doe: DeletesModelType,
shouldPersist: boolean = true shouldPersist = true
): Promise<void> { ): Promise<void> {
// Make sure the server timestamps for the DOE and the matching message // Make sure the server timestamps for the DOE and the matching message
// are less than one day apart // are less than one day apart

View file

@ -2,7 +2,7 @@ import moment from 'moment';
const HOUR = 1000 * 60 * 60; const HOUR = 1000 * 60 * 60;
export function formatDuration(seconds: number) { export function formatDuration(seconds: number): string {
const time = moment.utc(seconds * 1000); const time = moment.utc(seconds * 1000);
if (seconds > HOUR) { if (seconds > HOUR) {

View file

@ -35,7 +35,7 @@ function isYear(timestamp: moment.Moment) {
export function formatRelativeTime( export function formatRelativeTime(
rawTimestamp: number | Date, rawTimestamp: number | Date,
options: { extended?: boolean; i18n: LocalizerType } options: { extended?: boolean; i18n: LocalizerType }
) { ): string {
const { extended, i18n } = options; const { extended, i18n } = options;
const formats = extended ? getExtendedFormats(i18n) : getShortFormats(i18n); const formats = extended ? getExtendedFormats(i18n) : getShortFormats(i18n);
@ -45,13 +45,17 @@ export function formatRelativeTime(
if (diff.years() >= 1 || !isYear(timestamp)) { if (diff.years() >= 1 || !isYear(timestamp)) {
return replaceSuffix(timestamp.format(formats.y)); return replaceSuffix(timestamp.format(formats.y));
} else if (diff.months() >= 1 || diff.days() > 6) { }
if (diff.months() >= 1 || diff.days() > 6) {
return replaceSuffix(timestamp.format(formats.M)); return replaceSuffix(timestamp.format(formats.M));
} else if (diff.days() >= 1 || !isToday(timestamp)) { }
if (diff.days() >= 1 || !isToday(timestamp)) {
return replaceSuffix(timestamp.format(formats.d)); return replaceSuffix(timestamp.format(formats.d));
} else if (diff.hours() >= 1) { }
if (diff.hours() >= 1) {
return i18n('hoursAgo', [String(diff.hours())]); return i18n('hoursAgo', [String(diff.hours())]);
} else if (diff.minutes() >= 1) { }
if (diff.minutes() >= 1) {
return i18n('minutesAgo', [String(diff.minutes())]); return i18n('minutesAgo', [String(diff.minutes())]);
} }

View file

@ -7,14 +7,14 @@ function removeNonInitials(name: string) {
export function getInitials(name?: string): string | undefined { export function getInitials(name?: string): string | undefined {
if (!name) { if (!name) {
return; return undefined;
} }
const cleaned = removeNonInitials(name); const cleaned = removeNonInitials(name);
const parts = cleaned.split(' '); const parts = cleaned.split(' ');
const initials = parts.map(part => part.trim()[0]); const initials = parts.map(part => part.trim()[0]);
if (!initials.length) { if (!initials.length) {
return; return undefined;
} }
return initials.slice(0, 2).join(''); return initials.slice(0, 2).join('');

View file

@ -11,7 +11,7 @@ export function getStringForProfileChange(
change: ProfileNameChangeType, change: ProfileNameChangeType,
changedContact: ConversationType, changedContact: ConversationType,
i18n: LocalizerType i18n: LocalizerType
) { ): string {
if (change.type === 'name') { if (change.type === 'name') {
return changedContact.name return changedContact.name
? i18n('contactChangedProfileName', { ? i18n('contactChangedProfileName', {
@ -23,7 +23,7 @@ export function getStringForProfileChange(
oldProfile: change.oldName, oldProfile: change.oldName,
newProfile: change.newName, newProfile: change.newName,
}); });
} else {
throw new Error('TimelineItem: Unknown type!');
} }
throw new Error('TimelineItem: Unknown type!');
} }

View file

@ -2,7 +2,7 @@ const env = window.getEnvironment();
const NINETY_ONE_DAYS = 86400 * 91 * 1000; const NINETY_ONE_DAYS = 86400 * 91 * 1000;
export function hasExpired() { export function hasExpired(): boolean {
const { getExpiration, log } = window; const { getExpiration, log } = window;
let buildExpiration = 0; let buildExpiration = 0;
@ -31,5 +31,5 @@ export function hasExpired() {
return Date.now() > buildExpiration && tooFarIntoFuture; return Date.now() > buildExpiration && tooFarIntoFuture;
} }
return buildExpiration && Date.now() > buildExpiration; return buildExpiration !== 0 && Date.now() > buildExpiration;
} }

View file

@ -5,16 +5,17 @@ import { useDispatch } from 'react-redux';
// Restore focus on teardown // Restore focus on teardown
export const useRestoreFocus = ( export const useRestoreFocus = (
// The ref for the element to receive initial focus // The ref for the element to receive initial focus
focusRef: React.RefObject<any>, focusRef: React.RefObject<HTMLElement>,
// Allow for an optional root element that must exist // Allow for an optional root element that must exist
root: boolean | HTMLElement | null = true root: boolean | HTMLElement | null = true
) => { ): void => {
React.useEffect(() => { React.useEffect(() => {
if (!root) { if (!root) {
return; return undefined;
} }
const lastFocused = document.activeElement as any; const lastFocused = document.activeElement as HTMLElement;
if (focusRef.current) { if (focusRef.current) {
focusRef.current.focus(); focusRef.current.focus();
} }
@ -33,10 +34,10 @@ export const useRestoreFocus = (
export const useBoundActions = <T extends ActionCreatorsMapObject>( export const useBoundActions = <T extends ActionCreatorsMapObject>(
actions: T actions: T
) => { ): T => {
const dispatch = useDispatch(); const dispatch = useDispatch();
return React.useMemo(() => { return React.useMemo(() => {
return bindActionCreators(actions, dispatch); return bindActionCreators(actions, dispatch);
}, [dispatch]); }, [actions, dispatch]);
}; };

Some files were not shown because too many files have changed in this diff Show more