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
ts/**/*.js
sticker-creator/**/*.js
**/*.d.ts
webpack.config.ts
# Temporarily ignored during TSLint transition
# JIRA: DESKTOP-304
sticker-creator/**/*.ts
sticker-creator/**/*.tsx
ts/*.ts
ts/components/*.ts
ts/components/*.tsx
ts/components/stickers/**
ts/shims/**
ts/sql/**
ts/state/**
ts/storybook/**
ts/styleguide/**
ts/test/**
ts/textsecure/**
ts/types/**
ts/updater/**
ts/util/**

View file

@ -77,6 +77,8 @@ const rules = {
// Prefer functional components with default params
'react/require-default-props': 'off',
'jsx-a11y/label-has-associated-control': ['error', { assert: 'either' }],
};
module.exports = {
@ -94,7 +96,7 @@ module.exports = {
overrides: [
{
files: ['*.ts', '*.tsx'],
files: ['ts/**/*.ts', 'ts/**/*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
@ -113,12 +115,31 @@ module.exports = {
],
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/**'],
rules: {
...rules,
'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
'react/no-array-index-key': 'off',
},
},

1
.gitignore vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,5 @@
/* eslint-disable max-len */
import * as React from 'react';
import * as styles from './ShareButtons.scss';
import { useI18n } from '../util/i18n';
@ -6,64 +8,67 @@ export type Props = {
value: string;
};
export const ShareButtons = React.memo(({ value }: Props) => {
const i18n = useI18n();
export const ShareButtons: React.ComponentType<Props> = React.memo(
({ value }) => {
const i18n = useI18n();
const buttonPaths = React.useMemo<
Array<[string, string, string, string]>
>(() => {
const packUrl = encodeURIComponent(value);
const text = encodeURIComponent(
`${i18n('StickerCreator--ShareStage--socialMessage')} ${value}`
const buttonPaths = React.useMemo<
Array<[string, string, string, string]>
>(() => {
const packUrl = encodeURIComponent(value);
const text = encodeURIComponent(
`${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 { StoryRow } from '../elements/StoryRow';
import { StickerFrame } from './StickerFrame';
import { storiesOf } from '@storybook/react';
import { boolean, select, text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { StoryRow } from '../elements/StoryRow';
import { StickerFrame } from './StickerFrame';
storiesOf('Sticker Creator/components', module).add('StickerFrame', () => {
const image = text('image url', '/fixtures/512x515-thumbs-up-lincoln.webp');
const showGuide = boolean('show guide', true);
@ -16,7 +16,7 @@ storiesOf('Sticker Creator/components', module).add('StickerFrame', () => {
const [emoji, setEmoji] = React.useState(undefined);
return (
<StoryRow top={true}>
<StoryRow top>
<StickerFrame
id="1337"
emojiData={emoji}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,18 @@
import * as React from 'react';
import { debounce, dropRight } from 'lodash';
import { StoryRow } from '../elements/StoryRow';
import { Toaster } from './Toaster';
import { storiesOf } from '@storybook/react';
import { text as textKnob } from '@storybook/addon-knobs';
import { StoryRow } from '../elements/StoryRow';
import { Toaster } from './Toaster';
storiesOf('Sticker Creator/components', module).add('Toaster', () => {
const inputText = textKnob('Slices', ['error 1', 'error 2'].join('|'));
const initialState = React.useMemo(() => inputText.split('|'), [inputText]);
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(
// Debounce is required here since auto-dismiss is asynchronously called
// 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 { StoryRow } from './StoryRow';
import { Button } from './Button';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { text } from '@storybook/addon-knobs';
import { StoryRow } from './StoryRow';
import { Button } from './Button';
storiesOf('Sticker Creator/elements', module).add('Button', () => {
const onClick = action('onClick');
const child = text('text', 'foo bar');
@ -13,12 +13,12 @@ storiesOf('Sticker Creator/elements', module).add('Button', () => {
return (
<>
<StoryRow>
<Button onClick={onClick} primary={true}>
<Button onClick={onClick} primary>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} primary={true} disabled={true}>
<Button onClick={onClick} primary disabled>
{child}
</Button>
</StoryRow>
@ -26,27 +26,27 @@ storiesOf('Sticker Creator/elements', module).add('Button', () => {
<Button onClick={onClick}>{child}</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} disabled={true}>
<Button onClick={onClick} disabled>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} primary={true} pill={true}>
<Button onClick={onClick} primary pill>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} primary={true} pill={true} disabled={true}>
<Button onClick={onClick} primary pill disabled>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} pill={true}>
<Button onClick={onClick} pill>
{child}
</Button>
</StoryRow>
<StoryRow>
<Button onClick={onClick} pill={true} disabled={true}>
<Button onClick={onClick} pill disabled>
{child}
</Button>
</StoryRow>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
import * as React from 'react';
import { StoryRow } from './StoryRow';
import { Toast } from './Toast';
import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { StoryRow } from './StoryRow';
import { Toast } from './Toast';
storiesOf('Sticker Creator/elements', module).add('Toast', () => {
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) => (
<button className={classNames(styles.base, className)} {...rest}>
<button
type="button"
className={classNames(styles.base, className)}
{...rest}
>
{children}
</button>
));

View file

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

View file

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

View file

@ -4,5 +4,6 @@ import { Root } from './root';
const root = document.getElementById('root');
// eslint-disable-next-line no-console
console.log('Sticker Creator: Starting 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 * as React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
@ -8,7 +7,12 @@ import { history } from './util/history';
import { store } from './store';
import { I18n } from './util/i18n';
// @ts-ignore
declare global {
interface Window {
localeMessages: { [key: string]: { message: string } };
}
}
const { localeMessages } = window;
const ColdRoot = () => (

View file

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

View file

@ -1,4 +1,5 @@
import { combineReducers, Reducer } from 'redux';
// eslint-disable-next-line import/no-cycle
import { reducer as stickers } from './ducks/stickers';
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 } };
};
export const I18n = ({ messages, children }: I18nProps) => {
export const I18n = ({ messages, children }: I18nProps): JSX.Element => {
const getMessage = React.useCallback<I18nFn>(
(key, substitutions) => {
if (Array.isArray(substitutions) && substitutions.length > 1) {
@ -28,7 +28,8 @@ export const I18n = ({ messages, children }: I18nProps) => {
const { message } = messages[key];
if (!substitutions) {
return message;
} else if (Array.isArray(substitutions)) {
}
if (Array.isArray(substitutions)) {
return substitutions.reduce(
(result, substitution) =>
result.toString().replace(/\$.+?\$/, substitution.toString()),
@ -50,7 +51,7 @@ export const I18n = ({ messages, children }: I18nProps) => {
const placeholderName = match[1];
const value = substitutions[placeholderName];
if (!value) {
// tslint:disable-next-line no-console
// eslint-disable-next-line no-console
console.error(
`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';
declare global {
interface Window {
convertToWebp: ConvertToWebpFn;
encryptAndUpload: EncryptAndUploadFn;
}
}
export type WebpData = {
buffer: Buffer;
src: string;
@ -13,9 +20,6 @@ export type ConvertToWebpFn = (
height?: number
) => Promise<WebpData>;
// @ts-ignore
export const convertToWebp: ConvertToWebpFn = window.convertToWebp;
export type StickerData = { webp?: WebpData; emoji?: string };
export type PackMetaData = { packId: string; key: string };
@ -26,5 +30,4 @@ export type EncryptAndUploadFn = (
onProgress?: () => unknown
) => Promise<PackMetaData>;
// @ts-ignore
export const encryptAndUpload: EncryptAndUploadFn = window.encryptAndUpload;
export const { encryptAndUpload, convertToWebp } = window;

View file

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

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

@ -18,7 +18,9 @@ type DeletesAttributesType = {
targetSentTimestamp: number;
};
declare class DeletesModelType extends Backbone.Model<DeletesAttributesType> {
export declare class DeletesModelType extends Backbone.Model<
DeletesAttributesType
> {
forMessage(message: MessageModelType): Array<DeletesModelType>;
onDelete(doe: DeletesAttributesType): Promise<void>;
}
@ -47,7 +49,9 @@ export type MessageAttributesType = {
sourceUuid?: string;
};
declare class MessageModelType extends Backbone.Model<MessageAttributesType> {
export declare class MessageModelType extends Backbone.Model<
MessageAttributesType
> {
id: string;
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 { 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();
// So Redux logging doesn't go to disk, and so we can get colors/styles
const directConsole = {
// @ts-ignore
log: console._log,
groupCollapsed: console.groupCollapsed,
group: console.group,
@ -27,7 +38,7 @@ const logger = createLogger({
// Exclude logger if we're in production mode
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);

View file

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

View file

@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import {
difference,
fromPairs,
@ -113,7 +114,7 @@ export type MessageType = {
deletedForEveryone?: boolean;
errors?: Array<Error>;
group_update?: any;
group_update?: unknown;
// 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
@ -393,7 +394,10 @@ function removeAllConversations(): RemoveAllConversationsActionType {
};
}
function selectMessage(messageId: string, conversationId: string) {
function selectMessage(
messageId: string,
conversationId: string
): MessageSelectedActionType {
return {
type: 'MESSAGE_SELECTED',
payload: {
@ -567,13 +571,13 @@ function openConversationExternal(
};
}
function showInbox() {
function showInbox(): ShowInboxActionType {
return {
type: 'SHOW_INBOX',
payload: null,
};
}
function showArchivedConversations() {
function showArchivedConversations(): ShowArchivedConversationsActionType {
return {
type: 'SHOW_ARCHIVED_CONVERSATIONS',
payload: null,
@ -596,7 +600,7 @@ function getEmptyState(): ConversationsStateType {
function hasMessageHeightChanged(
message: MessageType,
previous: MessageType
): Boolean {
): boolean {
const messageAttachments = message.attachments || [];
const previousAttachments = previous.attachments || [];
@ -687,8 +691,7 @@ export function reducer(
const { id, data } = payload;
const { conversationLookup } = state;
let showArchived = state.showArchived;
let selectedConversation = state.selectedConversation;
let { showArchived, selectedConversation } = state;
const existing = conversationLookup[id];
// 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) {
oldest = pick(first, ['id', 'received_at']);
}

View file

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

View file

@ -9,7 +9,7 @@ import { useBoundActions } from '../../util/hooks';
// State
export type ItemsStateType = {
readonly [key: string]: any;
readonly [key: string]: unknown;
};
// Actions
@ -23,7 +23,7 @@ type ItemPutExternalAction = {
type: 'items/PUT_EXTERNAL';
payload: {
key: string;
value: any;
value: unknown;
};
};
@ -58,9 +58,9 @@ export const actions = {
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);
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 {
type: 'items/PUT_EXTERNAL',
payload: {
@ -139,4 +139,5 @@ const selectRecentEmojis = createSelector(
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) {
const conversation = searchResults[i];
if (conversation.type === 'private' && !Boolean(conversation.lastMessage)) {
if (conversation.type === 'private' && !conversation.lastMessage) {
contacts.push(conversation.id);
} else {
conversations.push(conversation.id);

View file

@ -376,7 +376,10 @@ export function reducer(
action: StickersActionType
): StickersStateType {
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 = {
stickers: {},
...payload,

View file

@ -92,5 +92,5 @@ export const reducers = {
user,
};
// @ts-ignore: AnyAction breaks strong type checking inside reducers
export const reducer = combineReducers<StateType, ActionsType>(reducers);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-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}>
<FilteredCallManager />
</Provider>

View file

@ -7,9 +7,14 @@ import { SmartCompositionArea } from '../smart/CompositionArea';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-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}>
<FilteredCompositionArea {...props} />
</Provider>

View file

@ -7,9 +7,11 @@ import { SmartLeftPane } from '../smart/LeftPane';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-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}>
<FilteredLeftPane />
</Provider>

View file

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

View file

@ -7,9 +7,14 @@ import { SmartShortcutGuideModal } from '../smart/ShortcutGuideModal';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-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}>
<FilteredShortcutGuideModal {...props} />
</Provider>

View file

@ -7,9 +7,11 @@ import { SmartStickerManager } from '../smart/StickerManager';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-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}>
<FilteredStickerManager />
</Provider>

View file

@ -7,9 +7,14 @@ import { SmartStickerPreviewModal } from '../smart/StickerPreviewModal';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-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}>
<FilteredStickerPreviewModal {...props} />
</Provider>

View file

@ -7,9 +7,14 @@ import { SmartTimeline } from '../smart/Timeline';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-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}>
<FilteredTimeline {...props} />
</Provider>

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ export const getUserUuid = createSelector(
export const getUserAgent = createSelector(
getItems,
(state: ItemsStateType): string => state.userAgent
(state: ItemsStateType): string => state.userAgent as string
);
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()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredCallingDeviceSelection = SmartCallingDeviceSelection as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
function renderDeviceSelection(): JSX.Element {
return <FilteredCallingDeviceSelection />;

View file

@ -89,4 +89,5 @@ const 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;
};
export const SmartContactName = (props: ExternalProps) => {
export const SmartContactName: React.ComponentType<ExternalProps> = props => {
const { conversationId } = props;
const i18n = useSelector<StateType, LocalizerType>(getIntl);
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()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
/* eslint-disable @typescript-eslint/no-explicit-any */
const FilteredSmartMainHeader = SmartMainHeader as any;
const FilteredSmartMessageSearchResult = SmartMessageSearchResult as any;
const FilteredSmartNetworkStatus = SmartNetworkStatus as any;
const FilteredSmartUpdateDialog = SmartUpdateDialog as any;
const FilteredSmartExpiredBuildDialog = SmartExpiredBuildDialog as any;
const FilteredSmartRelinkDialog = SmartRelinkDialog as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
function renderExpiredBuildDialog(): JSX.Element {
return <FilteredSmartExpiredBuildDialog />;

View file

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

View file

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

View file

@ -64,7 +64,7 @@ export function getExtensionForDisplay({
}
if (!contentType) {
return;
return undefined;
}
const slash = contentType.indexOf('/');
@ -72,10 +72,12 @@ export function getExtensionForDisplay({
return contentType.slice(slash + 1);
}
return;
return undefined;
}
export function isAudio(attachments?: Array<AttachmentType>) {
export function isAudio(
attachments?: Array<AttachmentType>
): boolean | undefined {
return (
attachments &&
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 } =
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) {
return attachment.thumbnail.url;
}
@ -106,7 +110,7 @@ export function getThumbnailUrl(attachment: AttachmentType) {
return getUrl(attachment);
}
export function getUrl(attachment: AttachmentType) {
export function getUrl(attachment: AttachmentType): string {
if (attachment.screenshot) {
return attachment.screenshot.url;
}
@ -114,7 +118,9 @@ export function getUrl(attachment: AttachmentType) {
return attachment.url;
}
export function isImage(attachments?: Array<AttachmentType>) {
export function isImage(
attachments?: Array<AttachmentType>
): boolean | undefined {
return (
attachments &&
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 (
attachment &&
attachment.contentType &&
isImageTypeSupported(attachment.contentType)
);
}
export function hasImage(attachments?: Array<AttachmentType>) {
export function hasImage(
attachments?: Array<AttachmentType>
): string | boolean | undefined {
return (
attachments &&
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]);
}
export function isVideoAttachment(attachment?: AttachmentType) {
export function isVideoAttachment(
attachment?: AttachmentType
): boolean | undefined {
return (
attachment &&
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;
return (
@ -308,7 +324,7 @@ export const isFile = (attachment: Attachment): boolean => {
export const isVoiceMessage = (attachment: Attachment): boolean => {
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
const hasFlag =
// tslint:disable-next-line no-bitwise
// eslint-disable-next-line no-bitwise
!is.undefined(attachment.flags) && (attachment.flags & flag) === flag;
if (hasFlag) {
return true;
@ -390,7 +406,7 @@ export const getFileExtension = (
attachment: Attachment
): string | undefined => {
if (!attachment.contentType) {
return;
return undefined;
}
switch (attachment.contentType) {

View file

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

View file

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

View file

@ -1,4 +1,4 @@
type LogFunction = (...args: Array<any>) => void;
type LogFunction = (...args: Array<unknown>) => void;
export type LoggerType = {
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_JSON = 'application/json' as MIMEType;

View file

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

View file

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

View file

@ -11,7 +11,8 @@ export enum AudioNotificationSupport {
export function getAudioNotificationSupport(): AudioNotificationSupport {
if (OS.isWindows(MIN_WINDOWS_VERSION) || OS.isMacOS()) {
return AudioNotificationSupport.Native;
} else if (OS.isLinux()) {
}
if (OS.isLinux()) {
return AudioNotificationSupport.Custom;
}
return AudioNotificationSupport.None;
@ -22,11 +23,11 @@ export const isAudioNotificationSupported = (): boolean =>
// Using `Notification::tag` has a bug on Windows 7:
// https://github.com/electron/electron/issues/11189
export const isNotificationGroupingSupported = () =>
export const isNotificationGroupingSupported = (): boolean =>
!OS.isWindows() || OS.isWindows(MIN_WINDOWS_VERSION);
// 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
export const isDrawAttentionSupported = () => !OS.isMacOS();
export const isDrawAttentionSupported = (): boolean => !OS.isMacOS();

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
export function cleanSearchTerm(searchTerm: string) {
export function cleanSearchTerm(searchTerm: string): string {
const lowercase = searchTerm.toLowerCase();
const withoutSpecialCharacters = lowercase.replace(
/([!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~])/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
// 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 {
// eslint-disable-next-line no-restricted-syntax
for (const codePoint of name) {
if (!isCKJV(codePoint)) {
return false;
@ -59,7 +62,6 @@ function isAllCKJV(name: string): boolean {
return true;
}
// tslint:disable-next-line cyclomatic-complexity
function isCKJV(codePoint: string) {
if (codePoint === ' ') {
return true;

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ export function getStringForProfileChange(
change: ProfileNameChangeType,
changedContact: ConversationType,
i18n: LocalizerType
) {
): string {
if (change.type === 'name') {
return changedContact.name
? i18n('contactChangedProfileName', {
@ -23,7 +23,7 @@ export function getStringForProfileChange(
oldProfile: change.oldName,
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;
export function hasExpired() {
export function hasExpired(): boolean {
const { getExpiration, log } = window;
let buildExpiration = 0;
@ -31,5 +31,5 @@ export function hasExpired() {
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
export const useRestoreFocus = (
// 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
root: boolean | HTMLElement | null = true
) => {
): void => {
React.useEffect(() => {
if (!root) {
return;
return undefined;
}
const lastFocused = document.activeElement as any;
const lastFocused = document.activeElement as HTMLElement;
if (focusRef.current) {
focusRef.current.focus();
}
@ -33,10 +34,10 @@ export const useRestoreFocus = (
export const useBoundActions = <T extends ActionCreatorsMapObject>(
actions: T
) => {
): T => {
const dispatch = useDispatch();
return React.useMemo(() => {
return bindActionCreators(actions, dispatch);
}, [dispatch]);
}, [actions, dispatch]);
};

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