Migrate components to eslint

This commit is contained in:
Chris Svenningsen 2020-09-11 17:46:52 -07:00 committed by Josh Perez
parent de66486e41
commit b13dbcfa77
69 changed files with 875 additions and 800 deletions

View file

@ -30,8 +30,9 @@ webpack.config.ts
# Temporarily ignored during TSLint transition # Temporarily ignored during TSLint transition
# JIRA: DESKTOP-304 # JIRA: DESKTOP-304
ts/components/*.ts sticker-creator/**/*.ts
ts/components/*.tsx sticker-creator/**/*.tsx
ts/*.ts
ts/components/conversation/** ts/components/conversation/**
ts/components/stickers/** ts/components/stickers/**
ts/shims/** ts/shims/**
@ -44,5 +45,3 @@ ts/textsecure/**
ts/types/** ts/types/**
ts/updater/** ts/updater/**
ts/util/** ts/util/**
sticker-creator/**/*.ts
sticker-creator/**/*.tsx

View file

@ -59,7 +59,24 @@ const rules = {
}, },
], ],
'react/jsx-props-no-spreading': 'off',
// Updated to reflect future airbnb standard
// Allows for declaring defaultProps inside a class
'react/static-property-placement': ['error', 'static public field'],
// JIRA: DESKTOP-657
'react/sort-comp': 'off',
// We don't have control over the media we're sharing, so can't require
// captions.
'jsx-a11y/media-has-caption': 'off',
// We prefer named exports
'import/prefer-default-export': 'off', 'import/prefer-default-export': 'off',
// Prefer functional components with default params
'react/require-default-props': 'off',
}; };
module.exports = { module.exports = {
@ -101,7 +118,6 @@ module.exports = {
rules: { rules: {
...rules, ...rules,
'import/no-extraneous-dependencies': 'off', 'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
}, },
}, },
], ],

View file

@ -671,6 +671,10 @@
"message": "Search", "message": "Search",
"description": "Placeholder text in the search input" "description": "Placeholder text in the search input"
}, },
"clearSearch": {
"message": "Clear Search",
"description": "Aria label for clear search button"
},
"searchIn": { "searchIn": {
"message": "Search in $conversationName$", "message": "Search in $conversationName$",
"description": "Shown in the search box before text is entered when searching in a specific conversation", "description": "Shown in the search box before text is entered when searching in a specific conversation",
@ -3568,5 +3572,25 @@
"example": "5" "example": "5"
} }
} }
},
"close": {
"message": "close",
"description": "Generic close label"
},
"previous": {
"message": "previous",
"description": "Generic previous label"
},
"next": {
"message": "next",
"description": "Generic next label"
},
"CompositionArea--expand": {
"message": "Expand",
"description": "Aria label for expanding composition area"
},
"CompositionArea--attach-file": {
"message": "Attach file",
"description": "Aria label for file attachment button in composition area"
} }
} }

View file

@ -1,14 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { Avatar, Props } from './Avatar';
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';
// @ts-ignore import { Avatar, Props } from './Avatar';
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { Colors, ColorType } from '../types/Colors'; import { Colors, ColorType } from '../types/Colors';

View file

@ -56,15 +56,16 @@ export class Avatar extends React.Component<Props, State> {
return state; return state;
} }
public handleImageError() { public handleImageError(): void {
// tslint:disable-next-line no-console window.log.info(
console.log('Avatar: Image failed to load; failing over to placeholder'); 'Avatar: Image failed to load; failing over to placeholder'
);
this.setState({ this.setState({
imageBroken: true, imageBroken: true,
}); });
} }
public renderImage() { public renderImage(): JSX.Element | null {
const { avatarPath, i18n, title } = this.props; const { avatarPath, i18n, title } = this.props;
const { imageBroken } = this.state; const { imageBroken } = this.state;
@ -81,7 +82,7 @@ export class Avatar extends React.Component<Props, State> {
); );
} }
public renderNoImage() { public renderNoImage(): JSX.Element {
const { const {
conversationType, conversationType,
name, name,
@ -129,7 +130,7 @@ export class Avatar extends React.Component<Props, State> {
); );
} }
public render() { public render(): JSX.Element {
const { const {
avatarPath, avatarPath,
color, color,
@ -151,7 +152,11 @@ export class Avatar extends React.Component<Props, State> {
if (onClick) { if (onClick) {
contents = ( contents = (
<button className="module-avatar-button" onClick={onClick}> <button
type="button"
className="module-avatar-button"
onClick={onClick}
>
{hasImage ? this.renderImage() : this.renderNoImage()} {hasImage ? this.renderImage() : this.renderNoImage()}
</button> </button>
); );

View file

@ -2,14 +2,11 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { AvatarPopup, Props } from './AvatarPopup';
import { Colors, ColorType } from '../types/Colors';
import { boolean, select, text } from '@storybook/addon-knobs'; import { boolean, select, text } from '@storybook/addon-knobs';
// @ts-ignore import { AvatarPopup, Props } from './AvatarPopup';
import { Colors, ColorType } from '../types/Colors';
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -17,7 +17,7 @@ export type Props = {
style: React.CSSProperties; style: React.CSSProperties;
} & AvatarProps; } & AvatarProps;
export const AvatarPopup = (props: Props) => { export const AvatarPopup = (props: Props): JSX.Element => {
const focusRef = React.useRef<HTMLButtonElement>(null); const focusRef = React.useRef<HTMLButtonElement>(null);
const { const {
i18n, i18n,
@ -54,6 +54,7 @@ export const AvatarPopup = (props: Props) => {
</div> </div>
<hr className="module-avatar-popup__divider" /> <hr className="module-avatar-popup__divider" />
<button <button
type="button"
ref={focusRef} ref={focusRef}
className="module-avatar-popup__item" className="module-avatar-popup__item"
onClick={onViewPreferences} onClick={onViewPreferences}
@ -68,7 +69,11 @@ export const AvatarPopup = (props: Props) => {
{i18n('mainMenuSettings')} {i18n('mainMenuSettings')}
</div> </div>
</button> </button>
<button className="module-avatar-popup__item" onClick={onViewArchive}> <button
type="button"
className="module-avatar-popup__item"
onClick={onViewArchive}
>
<div <div
className={classNames( className={classNames(
'module-avatar-popup__item__icon', 'module-avatar-popup__item__icon',

View file

@ -1,16 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { CallManager } from './CallManager'; import { CallManager } from './CallManager';
import { CallState } from '../types/Calling'; import { CallState } from '../types/Calling';
import { ColorType } from '../types/Colors'; import { ColorType } from '../types/Colors';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const callDetails = { const callDetails = {

View file

@ -1,17 +1,14 @@
import * as React from 'react'; import * as React from 'react';
import { CallState } from '../types/Calling';
import { ColorType } from '../types/Colors';
import { CallScreen } from './CallScreen';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean, select } from '@storybook/addon-knobs'; import { boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { CallState } from '../types/Calling';
import { ColorType } from '../types/Colors';
import { CallScreen } from './CallScreen';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const callDetails = { const callDetails = {

View file

@ -27,7 +27,7 @@ const CallingButton = ({
); );
return ( return (
<button className={className} onClick={onClick}> <button type="button" className={className} onClick={onClick}>
<div /> <div />
</button> </button>
); );
@ -55,9 +55,14 @@ type StateType = {
}; };
export class CallScreen extends React.Component<PropsType, StateType> { export class CallScreen extends React.Component<PropsType, StateType> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private interval: any; private interval: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private controlsFadeTimer: any; private controlsFadeTimer: any;
private readonly localVideoRef: React.RefObject<HTMLVideoElement>; private readonly localVideoRef: React.RefObject<HTMLVideoElement>;
private readonly remoteVideoRef: React.RefObject<HTMLCanvasElement>; private readonly remoteVideoRef: React.RefObject<HTMLCanvasElement>;
constructor(props: PropsType) { constructor(props: PropsType) {
@ -75,18 +80,22 @@ export class CallScreen extends React.Component<PropsType, StateType> {
this.remoteVideoRef = React.createRef(); this.remoteVideoRef = React.createRef();
} }
public componentDidMount() { public componentDidMount(): void {
const { setLocalPreview, setRendererCanvas } = this.props;
// It's really jump with a value of 500ms. // It's really jump with a value of 500ms.
this.interval = setInterval(this.updateAcceptedTimer, 100); this.interval = setInterval(this.updateAcceptedTimer, 100);
this.fadeControls(); this.fadeControls();
document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.handleKeyDown);
this.props.setLocalPreview({ element: this.localVideoRef }); setLocalPreview({ element: this.localVideoRef });
this.props.setRendererCanvas({ element: this.remoteVideoRef }); setRendererCanvas({ element: this.remoteVideoRef });
} }
public componentWillUnmount() { public componentWillUnmount(): void {
const { setLocalPreview, setRendererCanvas } = this.props;
document.removeEventListener('keydown', this.handleKeyDown); document.removeEventListener('keydown', this.handleKeyDown);
if (this.interval) { if (this.interval) {
@ -95,11 +104,12 @@ export class CallScreen extends React.Component<PropsType, StateType> {
if (this.controlsFadeTimer) { if (this.controlsFadeTimer) {
clearTimeout(this.controlsFadeTimer); clearTimeout(this.controlsFadeTimer);
} }
this.props.setLocalPreview({ element: undefined });
this.props.setRendererCanvas({ element: undefined }); setLocalPreview({ element: undefined });
setRendererCanvas({ element: undefined });
} }
updateAcceptedTimer = () => { updateAcceptedTimer = (): void => {
const { acceptedTime } = this.state; const { acceptedTime } = this.state;
const { callState } = this.props; const { callState } = this.props;
@ -119,7 +129,7 @@ export class CallScreen extends React.Component<PropsType, StateType> {
} }
}; };
handleKeyDown = (event: KeyboardEvent) => { handleKeyDown = (event: KeyboardEvent): void => {
const { callDetails } = this.props; const { callDetails } = this.props;
if (!callDetails) { if (!callDetails) {
@ -143,8 +153,10 @@ export class CallScreen extends React.Component<PropsType, StateType> {
} }
}; };
showControls = () => { showControls = (): void => {
if (!this.state.showControls) { const { showControls } = this.state;
if (!showControls) {
this.setState({ this.setState({
showControls: true, showControls: true,
}); });
@ -153,7 +165,7 @@ export class CallScreen extends React.Component<PropsType, StateType> {
this.fadeControls(); this.fadeControls();
}; };
fadeControls = () => { fadeControls = (): void => {
if (this.controlsFadeTimer) { if (this.controlsFadeTimer) {
clearTimeout(this.controlsFadeTimer); clearTimeout(this.controlsFadeTimer);
} }
@ -165,7 +177,7 @@ export class CallScreen extends React.Component<PropsType, StateType> {
}, 5000); }, 5000);
}; };
toggleAudio = () => { toggleAudio = (): void => {
const { callDetails, hasLocalAudio, setLocalAudio } = this.props; const { callDetails, hasLocalAudio, setLocalAudio } = this.props;
if (!callDetails) { if (!callDetails) {
@ -178,7 +190,7 @@ export class CallScreen extends React.Component<PropsType, StateType> {
}); });
}; };
toggleVideo = () => { toggleVideo = (): void => {
const { callDetails, hasLocalVideo, setLocalVideo } = this.props; const { callDetails, hasLocalVideo, setLocalVideo } = this.props;
if (!callDetails) { if (!callDetails) {
@ -188,7 +200,7 @@ export class CallScreen extends React.Component<PropsType, StateType> {
setLocalVideo({ callId: callDetails.callId, enabled: !hasLocalVideo }); setLocalVideo({ callId: callDetails.callId, enabled: !hasLocalVideo });
}; };
public render() { public render(): JSX.Element | null {
const { const {
callDetails, callDetails,
callState, callState,
@ -238,6 +250,7 @@ export class CallScreen extends React.Component<PropsType, StateType> {
{this.renderMessage(callState)} {this.renderMessage(callState)}
<div className="module-ongoing-call__settings"> <div className="module-ongoing-call__settings">
<button <button
type="button"
aria-label={i18n('callingDeviceSelection__settings')} aria-label={i18n('callingDeviceSelection__settings')}
className="module-ongoing-call__settings--button" className="module-ongoing-call__settings--button"
onClick={toggleSettings} onClick={toggleSettings}
@ -322,6 +335,7 @@ export class CallScreen extends React.Component<PropsType, StateType> {
private renderMessage(callState: CallState) { private renderMessage(callState: CallState) {
const { i18n } = this.props; const { i18n } = this.props;
const { acceptedDuration } = this.state;
let message = null; let message = null;
if (callState === CallState.Prering) { if (callState === CallState.Prering) {
@ -330,13 +344,8 @@ export class CallScreen extends React.Component<PropsType, StateType> {
message = i18n('outgoingCallRinging'); message = i18n('outgoingCallRinging');
} else if (callState === CallState.Reconnecting) { } else if (callState === CallState.Reconnecting) {
message = i18n('callReconnecting'); message = i18n('callReconnecting');
} else if ( } else if (callState === CallState.Accepted && acceptedDuration) {
callState === CallState.Accepted && message = i18n('callDuration', [this.renderDuration(acceptedDuration)]);
this.state.acceptedDuration
) {
message = i18n('callDuration', [
this.renderDuration(this.state.acceptedDuration),
]);
} }
if (!message) { if (!message) {
@ -345,6 +354,7 @@ export class CallScreen extends React.Component<PropsType, StateType> {
return <div className="module-ongoing-call__header-message">{message}</div>; return <div className="module-ongoing-call__header-message">{message}</div>;
} }
// eslint-disable-next-line class-methods-use-this
private renderDuration(ms: number): string { private renderDuration(ms: number): string {
const secs = Math.floor((ms / 1000) % 60) const secs = Math.floor((ms / 1000) % 60)
.toString() .toString()

View file

@ -1,14 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { CallingDeviceSelection, Props } from './CallingDeviceSelection';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { CallingDeviceSelection, Props } from './CallingDeviceSelection';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const audioDevice = { const audioDevice = {

View file

@ -31,7 +31,7 @@ function renderAudioOptions(
): JSX.Element { ): JSX.Element {
if (!devices.length) { if (!devices.length) {
return ( return (
<option aria-selected={true}> <option aria-selected>
{i18n('callingDeviceSelection__select--no-device')} {i18n('callingDeviceSelection__select--no-device')}
</option> </option>
); );
@ -63,7 +63,7 @@ function renderVideoOptions(
): JSX.Element { ): JSX.Element {
if (!devices.length) { if (!devices.length) {
return ( return (
<option aria-selected={true}> <option aria-selected>
{i18n('callingDeviceSelection__select--no-device')} {i18n('callingDeviceSelection__select--no-device')}
</option> </option>
); );
@ -134,9 +134,11 @@ export const CallingDeviceSelection = ({
<ConfirmationModal actions={[]} i18n={i18n} onClose={toggleSettings}> <ConfirmationModal actions={[]} i18n={i18n} onClose={toggleSettings}>
<div className="module-calling-device-selection"> <div className="module-calling-device-selection">
<button <button
type="button"
className="module-calling-device-selection__close-button" className="module-calling-device-selection__close-button"
onClick={toggleSettings} onClick={toggleSettings}
tabIndex={0} tabIndex={0}
aria-label={i18n('close')}
/> />
</div> </div>
@ -144,14 +146,13 @@ export const CallingDeviceSelection = ({
{i18n('callingDeviceSelection__settings')} {i18n('callingDeviceSelection__settings')}
</h1> </h1>
<label className="module-calling-device-selection__label"> <label htmlFor="video" className="module-calling-device-selection__label">
{i18n('callingDeviceSelection__label--video')} {i18n('callingDeviceSelection__label--video')}
</label> </label>
<div className="module-calling-device-selection__select"> <div className="module-calling-device-selection__select">
<select <select
disabled={!availableCameras.length} disabled={!availableCameras.length}
name="video" name="video"
// tslint:disable-next-line react-a11y-no-onchange
onChange={createCameraChangeHandler(changeIODevice)} onChange={createCameraChangeHandler(changeIODevice)}
value={selectedCamera} value={selectedCamera}
> >
@ -159,14 +160,16 @@ export const CallingDeviceSelection = ({
</select> </select>
</div> </div>
<label className="module-calling-device-selection__label"> <label
htmlFor="audio-input"
className="module-calling-device-selection__label"
>
{i18n('callingDeviceSelection__label--audio-input')} {i18n('callingDeviceSelection__label--audio-input')}
</label> </label>
<div className="module-calling-device-selection__select"> <div className="module-calling-device-selection__select">
<select <select
disabled={!availableMicrophones.length} disabled={!availableMicrophones.length}
name="audio-input" name="audio-input"
// tslint:disable-next-line react-a11y-no-onchange
onChange={createAudioChangeHandler( onChange={createAudioChangeHandler(
availableMicrophones, availableMicrophones,
changeIODevice, changeIODevice,
@ -178,14 +181,16 @@ export const CallingDeviceSelection = ({
</select> </select>
</div> </div>
<label className="module-calling-device-selection__label"> <label
htmlFor="audio-output"
className="module-calling-device-selection__label"
>
{i18n('callingDeviceSelection__label--audio-output')} {i18n('callingDeviceSelection__label--audio-output')}
</label> </label>
<div className="module-calling-device-selection__select"> <div className="module-calling-device-selection__select">
<select <select
disabled={!availableSpeakers.length} disabled={!availableSpeakers.length}
name="audio-output" name="audio-output"
// tslint:disable-next-line react-a11y-no-onchange
onChange={createAudioChangeHandler( onChange={createAudioChangeHandler(
availableSpeakers, availableSpeakers,
changeIODevice, changeIODevice,

View file

@ -6,11 +6,7 @@ import { action } from '@storybook/addon-actions';
import { CaptionEditor, Props } from './CaptionEditor'; import { CaptionEditor, Props } from './CaptionEditor';
import { AUDIO_MP3, IMAGE_JPEG, VIDEO_MP4 } from '../types/MIME'; import { AUDIO_MP3, IMAGE_JPEG, VIDEO_MP4 } from '../types/MIME';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -1,5 +1,3 @@
// tslint:disable:react-a11y-anchors
import React from 'react'; import React from 'react';
import * as GoogleChrome from '../util/GoogleChrome'; import * as GoogleChrome from '../util/GoogleChrome';
@ -24,11 +22,15 @@ export class CaptionEditor extends React.Component<Props, State> {
private readonly handleKeyDownBound: ( private readonly handleKeyDownBound: (
event: React.KeyboardEvent<HTMLInputElement> event: React.KeyboardEvent<HTMLInputElement>
) => void; ) => void;
private readonly setFocusBound: () => void; private readonly setFocusBound: () => void;
private readonly onChangeBound: ( private readonly onChangeBound: (
event: React.FormEvent<HTMLInputElement> event: React.FormEvent<HTMLInputElement>
) => void; ) => void;
private readonly onSaveBound: () => void; private readonly onSaveBound: () => void;
private readonly inputRef: React.RefObject<HTMLInputElement>; private readonly inputRef: React.RefObject<HTMLInputElement>;
constructor(props: Props) { constructor(props: Props) {
@ -46,14 +48,14 @@ export class CaptionEditor extends React.Component<Props, State> {
this.inputRef = React.createRef(); this.inputRef = React.createRef();
} }
public componentDidMount() { public componentDidMount(): void {
// Forcing focus after a delay due to some focus contention with ConversationView // Forcing focus after a delay due to some focus contention with ConversationView
setTimeout(() => { setTimeout(() => {
this.setFocus(); this.setFocus();
}, 200); }, 200);
} }
public handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) { public handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
const { close, onSave } = this.props; const { close, onSave } = this.props;
if (close && event.key === 'Escape') { if (close && event.key === 'Escape') {
@ -72,13 +74,13 @@ export class CaptionEditor extends React.Component<Props, State> {
} }
} }
public setFocus() { public setFocus(): void {
if (this.inputRef.current) { if (this.inputRef.current) {
this.inputRef.current.focus(); this.inputRef.current.focus();
} }
} }
public onSave() { public onSave(): void {
const { onSave } = this.props; const { onSave } = this.props;
const { caption } = this.state; const { caption } = this.state;
@ -87,16 +89,15 @@ export class CaptionEditor extends React.Component<Props, State> {
} }
} }
public onChange(event: React.FormEvent<HTMLInputElement>) { public onChange(event: React.FormEvent<HTMLInputElement>): void {
// @ts-ignore const { value } = event.target as HTMLInputElement;
const { value } = event.target;
this.setState({ this.setState({
caption: value, caption: value,
}); });
} }
public renderObject() { public renderObject(): JSX.Element {
const { url, i18n, attachment } = this.props; const { url, i18n, attachment } = this.props;
const { contentType } = attachment || { contentType: null }; const { contentType } = attachment || { contentType: null };
@ -114,7 +115,7 @@ export class CaptionEditor extends React.Component<Props, State> {
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType); const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
if (isVideoTypeSupported) { if (isVideoTypeSupported) {
return ( return (
<video className="module-caption-editor__video" controls={true}> <video className="module-caption-editor__video" controls>
<source src={url} /> <source src={url} />
</video> </video>
); );
@ -123,14 +124,16 @@ export class CaptionEditor extends React.Component<Props, State> {
return <div className="module-caption-editor__placeholder" />; return <div className="module-caption-editor__placeholder" />;
} }
public render() { // Events handled by props
/* eslint-disable jsx-a11y/click-events-have-key-events */
public render(): JSX.Element {
const { i18n, close } = this.props; const { i18n, close } = this.props;
const { caption } = this.state; const { caption } = this.state;
const onKeyDown = close ? this.handleKeyDownBound : undefined; const onKeyDown = close ? this.handleKeyDownBound : undefined;
return ( return (
<div <div
role="dialog" role="presentation"
onClick={this.setFocusBound} onClick={this.setFocusBound}
className="module-caption-editor" className="module-caption-editor"
> >
@ -139,6 +142,8 @@ export class CaptionEditor extends React.Component<Props, State> {
role="button" role="button"
onClick={close} onClick={close}
className="module-caption-editor__close-button" className="module-caption-editor__close-button"
tabIndex={0}
aria-label={i18n('close')}
/> />
<div className="module-caption-editor__media-container"> <div className="module-caption-editor__media-container">
{this.renderObject()} {this.renderObject()}
@ -157,6 +162,7 @@ export class CaptionEditor extends React.Component<Props, State> {
/> />
{caption ? ( {caption ? (
<button <button
type="button"
onClick={this.onSaveBound} onClick={this.onSaveBound}
className="module-caption-editor__save-button" className="module-caption-editor__save-button"
> >
@ -168,4 +174,5 @@ export class CaptionEditor extends React.Component<Props, State> {
</div> </div>
); );
} }
/* eslint-enable jsx-a11y/click-events-have-key-events */
} }

View file

@ -1,19 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import 'draft-js/dist/Draft.css';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { boolean } from '@storybook/addon-knobs';
import { CompositionArea, Props } from './CompositionArea'; import { CompositionArea, Props } from './CompositionArea';
// tslint:disable-next-line
import 'draft-js/dist/Draft.css';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { boolean } from '@storybook/addon-knobs';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -91,6 +85,7 @@ story.add('Starting Text', () => {
story.add('Sticker Button', () => { story.add('Sticker Button', () => {
const props = createProps({ const props = createProps({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
knownPacks: [{} as any], knownPacks: [{} as any],
}); });

View file

@ -76,11 +76,11 @@ export type Props = Pick<
OwnProps; OwnProps;
const emptyElement = (el: HTMLElement) => { const emptyElement = (el: HTMLElement) => {
// tslint:disable-next-line no-inner-html // Necessary to deal with Backbone views
// eslint-disable-next-line no-param-reassign
el.innerHTML = ''; el.innerHTML = '';
}; };
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
export const CompositionArea = ({ export const CompositionArea = ({
i18n, i18n,
attachmentListEl, attachmentListEl,
@ -127,7 +127,7 @@ export const CompositionArea = ({
phoneNumber, phoneNumber,
profileName, profileName,
title, title,
}: Props) => { }: Props): JSX.Element => {
const [disabled, setDisabled] = React.useState(false); const [disabled, setDisabled] = React.useState(false);
const [showMic, setShowMic] = React.useState(!startingText); const [showMic, setShowMic] = React.useState(!startingText);
const [micActive, setMicActive] = React.useState(false); const [micActive, setMicActive] = React.useState(false);
@ -169,6 +169,8 @@ export const CompositionArea = ({
const attSlotRef = React.useRef<HTMLDivElement>(null); const attSlotRef = React.useRef<HTMLDivElement>(null);
if (compositionApi) { if (compositionApi) {
// Using a React.MutableRefObject, so we need to reassign this prop.
// eslint-disable-next-line no-param-reassign
compositionApi.current = { compositionApi.current = {
isDirty: () => dirty, isDirty: () => dirty,
focusInput, focusInput,
@ -255,7 +257,12 @@ export const CompositionArea = ({
const attButton = ( const attButton = (
<div className="module-composition-area__button-cell"> <div className="module-composition-area__button-cell">
<div className="choose-file"> <div className="choose-file">
<button className="paperclip thumbnail" onClick={onChooseAttachment} /> <button
type="button"
className="paperclip thumbnail"
onClick={onChooseAttachment}
aria-label={i18n('CompositionArea--attach-file')}
/>
</div> </div>
</div> </div>
); );
@ -268,8 +275,10 @@ export const CompositionArea = ({
)} )}
> >
<button <button
type="button"
className="module-composition-area__send-button" className="module-composition-area__send-button"
onClick={handleForceSend} onClick={handleForceSend}
aria-label={i18n('sendMessageToContact')}
/> />
</div> </div>
); );
@ -343,6 +352,7 @@ export const CompositionArea = ({
<div className="module-composition-area"> <div className="module-composition-area">
<div className="module-composition-area__toggle-large"> <div className="module-composition-area__toggle-large">
<button <button
type="button"
className={classNames( className={classNames(
'module-composition-area__toggle-large__button', 'module-composition-area__toggle-large__button',
large large
@ -352,6 +362,7 @@ export const CompositionArea = ({
// This prevents the user from tabbing here // This prevents the user from tabbing here
tabIndex={-1} tabIndex={-1}
onClick={handleToggleLarge} onClick={handleToggleLarge}
aria-label={i18n('CompositionArea--expand')}
/> />
</div> </div>
<div <div

View file

@ -1,19 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import 'draft-js/dist/Draft.css';
import { boolean, select } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { CompositionInput, Props } from './CompositionInput'; import { CompositionInput, Props } from './CompositionInput';
// tslint:disable-next-line
import 'draft-js/dist/Draft.css';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { boolean, select } from '@storybook/addon-knobs';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -63,7 +63,7 @@ function getTrimmedMatchAtIndex(str: string, index: number, pattern: RegExp) {
// Reset regex state // Reset regex state
pattern.exec(''); pattern.exec('');
// tslint:disable-next-line no-conditional-assignment // eslint-disable-next-line no-cond-assign
while ((match = pattern.exec(str))) { while ((match = pattern.exec(str))) {
const matchStr = match.toString(); const matchStr = match.toString();
const start = match.index + (matchStr.length - matchStr.trimLeft().length); const start = match.index + (matchStr.length - matchStr.trimLeft().length);
@ -155,7 +155,7 @@ const compositeDecorator = new CompositeDecorator([
const text = block.getText(); const text = block.getText();
let match; let match;
let index; let index;
// tslint:disable-next-line no-conditional-assignment // eslint-disable-next-line no-cond-assign
while ((match = pat.exec(text))) { while ((match = pat.exec(text))) {
index = match.index; index = match.index;
cb(index, index + match[0].length); cb(index, index + match[0].length);
@ -174,7 +174,7 @@ const compositeDecorator = new CompositeDecorator([
<Emoji <Emoji
shortName={contentState.getEntity(entityKey).getData().shortName} shortName={contentState.getEntity(entityKey).getData().shortName}
skinTone={contentState.getEntity(entityKey).getData().skinTone} skinTone={contentState.getEntity(entityKey).getData().skinTone}
inline={true} inline
size={20} size={20}
> >
{children} {children}
@ -204,7 +204,6 @@ const getInitialEditorState = (startingText?: string) => {
return EditorState.forceSelection(state, selectionAtEnd); return EditorState.forceSelection(state, selectionAtEnd);
}; };
// tslint:disable-next-line max-func-body-length
export const CompositionInput = ({ export const CompositionInput = ({
i18n, i18n,
disabled, disabled,
@ -221,7 +220,7 @@ export const CompositionInput = ({
startingText, startingText,
getQuotedMessage, getQuotedMessage,
clearQuotedMessage, clearQuotedMessage,
}: Props) => { }: Props): JSX.Element => {
const [editorRenderState, setEditorRenderState] = React.useState( const [editorRenderState, setEditorRenderState] = React.useState(
getInitialEditorState(startingText) getInitialEditorState(startingText)
); );
@ -299,119 +298,18 @@ export const CompositionInput = ({
setSearchText(''); setSearchText('');
}, [setEmojiResults, setEmojiResultsIndex, setSearchText]); }, [setEmojiResults, setEmojiResultsIndex, setSearchText]);
const handleEditorStateChange = React.useCallback( const getWordAtCaret = React.useCallback((state = editorStateRef.current) => {
(newState: EditorState) => { const selection = state.getSelection();
// Does the current position have any emojiable text? const index = selection.getAnchorOffset();
const selection = newState.getSelection();
const caretLocation = selection.getStartOffset(); return getWordAtIndex(
const content = newState state
.getCurrentContent() .getCurrentContent()
.getBlockForKey(selection.getAnchorKey()) .getBlockForKey(selection.getAnchorKey())
.getText(); .getText(),
const match = getTrimmedMatchAtIndex(content, caretLocation, colonsRegex); index
);
// Update the state to indicate emojiable text at the current position. }, []);
const newSearchText = match ? match.trim().substr(1) : '';
if (newSearchText.endsWith(':')) {
const bareText = trimEnd(newSearchText, ':');
const emoji = head(search(bareText));
if (emoji && bareText === emoji.short_name) {
handleEditorCommand('enter-emoji', newState, emoji);
// Prevent inserted colon from persisting to state
return;
} else {
resetEmojiResults();
}
} else if (triggerEmojiRegex.test(newSearchText) && focusRef.current) {
setEmojiResults(search(newSearchText, 10));
setSearchText(newSearchText);
setEmojiResultsIndex(0);
} else {
resetEmojiResults();
}
// Finally, update the editor state
setAndTrackEditorState(newState);
updateExternalStateListeners(newState);
},
[
focusRef,
resetEmojiResults,
setAndTrackEditorState,
setSearchText,
setEmojiResults,
]
);
const handleBeforeInput = React.useCallback((): DraftHandleValue => {
if (!editorStateRef.current) {
return 'not-handled';
}
const editorState = editorStateRef.current;
const plainText = editorState.getCurrentContent().getPlainText();
const selectedTextLength = getLengthOfSelectedText(editorState);
if (plainText.length - selectedTextLength > MAX_LENGTH - 1) {
onTextTooLong();
return 'handled';
}
return 'not-handled';
}, [onTextTooLong, editorStateRef]);
const handlePastedText = React.useCallback(
(pastedText: string): DraftHandleValue => {
if (!editorStateRef.current) {
return 'not-handled';
}
const editorState = editorStateRef.current;
const plainText = editorState.getCurrentContent().getPlainText();
const selectedTextLength = getLengthOfSelectedText(editorState);
if (
plainText.length + pastedText.length - selectedTextLength >
MAX_LENGTH
) {
onTextTooLong();
return 'handled';
}
return 'not-handled';
},
[onTextTooLong, editorStateRef]
);
const resetEditorState = React.useCallback(() => {
const newEmptyState = EditorState.createEmpty(compositeDecorator);
setAndTrackEditorState(newEmptyState);
resetEmojiResults();
}, [editorStateRef, resetEmojiResults, setAndTrackEditorState]);
const submit = React.useCallback(() => {
const { current: state } = editorStateRef;
const trimmedText = state
.getCurrentContent()
.getPlainText()
.trim();
onSubmit(trimmedText);
}, [editorStateRef, onSubmit]);
const handleEditorSizeChange = React.useCallback(
(rect: ContentRect) => {
if (rect.bounds) {
setEditorWidth(rect.bounds.width);
if (onEditorSizeChange) {
onEditorSizeChange(rect);
}
}
},
[onEditorSizeChange, setEditorWidth]
);
const selectEmojiResult = React.useCallback( const selectEmojiResult = React.useCallback(
(dir: 'next' | 'prev', e?: React.KeyboardEvent) => { (dir: 'next' | 'prev', e?: React.KeyboardEvent) => {
@ -445,93 +343,17 @@ export const CompositionInput = ({
} }
} }
}, },
[emojiResultsIndex, emojiResults] [emojiResults]
); );
const handleEditorArrowKey = React.useCallback( const submit = React.useCallback(() => {
(e: React.KeyboardEvent) => { const { current: state } = editorStateRef;
if (e.key === 'ArrowUp') { const trimmedText = state
selectEmojiResult('prev', e); .getCurrentContent()
} .getPlainText()
.trim();
if (e.key === 'ArrowDown') { onSubmit(trimmedText);
selectEmojiResult('next', e); }, [editorStateRef, onSubmit]);
}
},
[selectEmojiResult]
);
const handleEscapeKey = React.useCallback(
(e: React.KeyboardEvent) => {
if (emojiResults.length > 0) {
e.preventDefault();
resetEmojiResults();
} else if (getQuotedMessage()) {
clearQuotedMessage();
}
},
[resetEmojiResults, emojiResults]
);
const getWordAtCaret = React.useCallback((state = editorStateRef.current) => {
const selection = state.getSelection();
const index = selection.getAnchorOffset();
return getWordAtIndex(
state
.getCurrentContent()
.getBlockForKey(selection.getAnchorKey())
.getText(),
index
);
}, []);
const insertEmoji = React.useCallback(
(e: EmojiPickDataType, replaceWord: boolean = false) => {
const { current: state } = editorStateRef;
const selection = state.getSelection();
const oldContent = state.getCurrentContent();
const emojiContent = convertShortName(e.shortName, e.skinTone);
const emojiEntityKey = oldContent
.createEntity('emoji', 'IMMUTABLE', {
shortName: e.shortName,
skinTone: e.skinTone,
})
.getLastCreatedEntityKey();
const word = getWordAtCaret();
let newContent = Modifier.replaceText(
oldContent,
replaceWord
? (selection.merge({
anchorOffset: word.start,
focusOffset: word.end,
}) as SelectionState)
: selection,
emojiContent,
undefined,
emojiEntityKey
);
const afterSelection = newContent.getSelectionAfter();
if (
afterSelection.getAnchorOffset() ===
newContent.getBlockForKey(afterSelection.getAnchorKey()).getLength()
) {
newContent = Modifier.insertText(newContent, afterSelection, ' ');
}
const newState = EditorState.push(
state,
newContent,
'insert-emoji' as EditorChangeType
);
setAndTrackEditorState(newState);
resetEmojiResults();
},
[editorStateRef, setAndTrackEditorState, resetEmojiResults]
);
const handleEditorCommand = React.useCallback( const handleEditorCommand = React.useCallback(
( (
@ -604,9 +426,12 @@ export const CompositionInput = ({
return 'not-handled'; return 'not-handled';
}, },
// Missing `onPickEmoji`, which is a prop, so not clearly memoized
// eslint-disable-next-line react-hooks/exhaustive-deps
[ [
emojiResults, emojiResults,
emojiResultsIndex, emojiResultsIndex,
getWordAtCaret,
resetEmojiResults, resetEmojiResults,
selectEmojiResult, selectEmojiResult,
setAndTrackEditorState, setAndTrackEditorState,
@ -615,6 +440,184 @@ export const CompositionInput = ({
] ]
); );
const handleEditorStateChange = React.useCallback(
(newState: EditorState) => {
// Does the current position have any emojiable text?
const selection = newState.getSelection();
const caretLocation = selection.getStartOffset();
const content = newState
.getCurrentContent()
.getBlockForKey(selection.getAnchorKey())
.getText();
const match = getTrimmedMatchAtIndex(content, caretLocation, colonsRegex);
// Update the state to indicate emojiable text at the current position.
const newSearchText = match ? match.trim().substr(1) : '';
if (newSearchText.endsWith(':')) {
const bareText = trimEnd(newSearchText, ':');
const emoji = head(search(bareText));
if (emoji && bareText === emoji.short_name) {
handleEditorCommand('enter-emoji', newState, emoji);
// Prevent inserted colon from persisting to state
return;
}
resetEmojiResults();
} else if (triggerEmojiRegex.test(newSearchText) && focusRef.current) {
setEmojiResults(search(newSearchText, 10));
setSearchText(newSearchText);
setEmojiResultsIndex(0);
} else {
resetEmojiResults();
}
// Finally, update the editor state
setAndTrackEditorState(newState);
updateExternalStateListeners(newState);
},
[
focusRef,
handleEditorCommand,
resetEmojiResults,
setAndTrackEditorState,
setSearchText,
setEmojiResults,
updateExternalStateListeners,
]
);
const handleBeforeInput = React.useCallback((): DraftHandleValue => {
if (!editorStateRef.current) {
return 'not-handled';
}
const editorState = editorStateRef.current;
const plainText = editorState.getCurrentContent().getPlainText();
const selectedTextLength = getLengthOfSelectedText(editorState);
if (plainText.length - selectedTextLength > MAX_LENGTH - 1) {
onTextTooLong();
return 'handled';
}
return 'not-handled';
}, [onTextTooLong, editorStateRef]);
const handlePastedText = React.useCallback(
(pastedText: string): DraftHandleValue => {
if (!editorStateRef.current) {
return 'not-handled';
}
const editorState = editorStateRef.current;
const plainText = editorState.getCurrentContent().getPlainText();
const selectedTextLength = getLengthOfSelectedText(editorState);
if (
plainText.length + pastedText.length - selectedTextLength >
MAX_LENGTH
) {
onTextTooLong();
return 'handled';
}
return 'not-handled';
},
[onTextTooLong, editorStateRef]
);
const resetEditorState = React.useCallback(() => {
const newEmptyState = EditorState.createEmpty(compositeDecorator);
setAndTrackEditorState(newEmptyState);
resetEmojiResults();
}, [resetEmojiResults, setAndTrackEditorState]);
const handleEditorSizeChange = React.useCallback(
(rect: ContentRect) => {
if (rect.bounds) {
setEditorWidth(rect.bounds.width);
if (onEditorSizeChange) {
onEditorSizeChange(rect);
}
}
},
[onEditorSizeChange, setEditorWidth]
);
const handleEditorArrowKey = React.useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'ArrowUp') {
selectEmojiResult('prev', e);
}
if (e.key === 'ArrowDown') {
selectEmojiResult('next', e);
}
},
[selectEmojiResult]
);
const handleEscapeKey = React.useCallback(
(e: React.KeyboardEvent) => {
if (emojiResults.length > 0) {
e.preventDefault();
resetEmojiResults();
} else if (getQuotedMessage()) {
clearQuotedMessage();
}
},
[clearQuotedMessage, emojiResults, getQuotedMessage, resetEmojiResults]
);
const insertEmoji = React.useCallback(
(e: EmojiPickDataType, replaceWord = false) => {
const { current: state } = editorStateRef;
const selection = state.getSelection();
const oldContent = state.getCurrentContent();
const emojiContent = convertShortName(e.shortName, e.skinTone);
const emojiEntityKey = oldContent
.createEntity('emoji', 'IMMUTABLE', {
shortName: e.shortName,
skinTone: e.skinTone,
})
.getLastCreatedEntityKey();
const word = getWordAtCaret();
let newContent = Modifier.replaceText(
oldContent,
replaceWord
? (selection.merge({
anchorOffset: word.start,
focusOffset: word.end,
}) as SelectionState)
: selection,
emojiContent,
undefined,
emojiEntityKey
);
const afterSelection = newContent.getSelectionAfter();
if (
afterSelection.getAnchorOffset() ===
newContent.getBlockForKey(afterSelection.getAnchorKey()).getLength()
) {
newContent = Modifier.insertText(newContent, afterSelection, ' ');
}
const newState = EditorState.push(
state,
newContent,
'insert-emoji' as EditorChangeType
);
setAndTrackEditorState(newState);
resetEmojiResults();
},
[editorStateRef, getWordAtCaret, setAndTrackEditorState, resetEmojiResults]
);
const onTab = React.useCallback( const onTab = React.useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
if (e.shiftKey || emojiResults.length === 0) { if (e.shiftKey || emojiResults.length === 0) {
@ -624,11 +627,10 @@ export const CompositionInput = ({
e.preventDefault(); e.preventDefault();
handleEditorCommand('enter-emoji', editorStateRef.current); handleEditorCommand('enter-emoji', editorStateRef.current);
}, },
[emojiResults, editorStateRef, handleEditorCommand, resetEmojiResults] [emojiResults, editorStateRef, handleEditorCommand]
); );
const editorKeybindingFn = React.useCallback( const editorKeybindingFn = React.useCallback(
// tslint:disable-next-line cyclomatic-complexity
(e: React.KeyboardEvent): CompositionInputEditorCommand | null => { (e: React.KeyboardEvent): CompositionInputEditorCommand | null => {
const commandKey = get(window, 'platform') === 'darwin' && e.metaKey; const commandKey = get(window, 'platform') === 'darwin' && e.metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && e.ctrlKey; const controlKey = get(window, 'platform') !== 'darwin' && e.ctrlKey;
@ -718,7 +720,8 @@ export const CompositionInput = ({
// Manage focus // Manage focus
// Chromium places the editor caret at the beginning of contenteditable divs on focus // Chromium places the editor caret at the beginning of contenteditable divs on focus
// Here, we force the last known selection on focusin (doing this with onFocus wasn't behaving properly) // Here, we force the last known selection on focusin
// (doing this with onFocus wasn't behaving properly)
// This needs to be done in an effect because React doesn't support focus{In,Out} // This needs to be done in an effect because React doesn't support focus{In,Out}
// https://github.com/facebook/react/issues/6410 // https://github.com/facebook/react/issues/6410
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
@ -744,6 +747,8 @@ export const CompositionInput = ({
}, [editorStateRef, rootElRef, setAndTrackEditorState]); }, [editorStateRef, rootElRef, setAndTrackEditorState]);
if (inputApi) { if (inputApi) {
// Using a React.MutableRefObject, so we need to reassign this prop.
// eslint-disable-next-line no-param-reassign
inputApi.current = { inputApi.current = {
reset: resetEditorState, reset: resetEditorState,
submit, submit,
@ -756,7 +761,7 @@ export const CompositionInput = ({
<Manager> <Manager>
<Reference> <Reference>
{({ ref: popperRef }) => ( {({ ref: popperRef }) => (
<Measure bounds={true} onResize={handleEditorSizeChange}> <Measure bounds onResize={handleEditorSizeChange}>
{({ measureRef }) => ( {({ measureRef }) => (
<div <div
className="module-composition-input__input" className="module-composition-input__input"
@ -783,8 +788,8 @@ export const CompositionInput = ({
handleBeforeInput={handleBeforeInput} handleBeforeInput={handleBeforeInput}
handlePastedText={handlePastedText} handlePastedText={handlePastedText}
keyBindingFn={editorKeybindingFn} keyBindingFn={editorKeybindingFn}
spellCheck={true} spellCheck
stripPastedStyles={true} stripPastedStyles
readOnly={disabled} readOnly={disabled}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
@ -807,11 +812,13 @@ export const CompositionInput = ({
width: editorWidth, width: editorWidth,
}} }}
role="listbox" role="listbox"
aria-expanded={true} aria-expanded
aria-activedescendant={`emoji-result--${emojiResults[emojiResultsIndex].short_name}`} aria-activedescendant={`emoji-result--${emojiResults[emojiResultsIndex].short_name}`}
tabIndex={0}
> >
{emojiResults.map((emoji, index) => ( {emojiResults.map((emoji, index) => (
<button <button
type="button"
key={emoji.short_name} key={emoji.short_name}
id={`emoji-result--${emoji.short_name}`} id={`emoji-result--${emoji.short_name}`}
role="option button" role="option button"

View file

@ -1,15 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { ConfirmationDialog } from './ConfirmationDialog';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
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 { ConfirmationDialog } from './ConfirmationDialog';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
storiesOf('Components/ConfirmationDialog', module).add( storiesOf('Components/ConfirmationDialog', module).add(

View file

@ -73,6 +73,7 @@ export const ConfirmationDialog = React.memo(
{actions.length > 0 && ( {actions.length > 0 && (
<div className="module-confirmation-dialog__container__buttons"> <div className="module-confirmation-dialog__container__buttons">
<button <button
type="button"
onClick={handleCancel} onClick={handleCancel}
ref={focusRef} ref={focusRef}
className="module-confirmation-dialog__container__buttons__button" className="module-confirmation-dialog__container__buttons__button"
@ -81,7 +82,8 @@ export const ConfirmationDialog = React.memo(
</button> </button>
{actions.map((action, i) => ( {actions.map((action, i) => (
<button <button
key={i} type="button"
key={action.text}
onClick={handleAction} onClick={handleAction}
data-action={i} data-action={i}
className={classNames( className={classNames(

View file

@ -14,7 +14,6 @@ export type OwnProps = {
export type Props = OwnProps & ConfirmationDialogProps; export type Props = OwnProps & ConfirmationDialogProps;
export const ConfirmationModal = React.memo( export const ConfirmationModal = React.memo(
// tslint:disable-next-line max-func-body-length
({ i18n, onClose, children, ...rest }: Props) => { ({ i18n, onClose, children, ...rest }: Props) => {
const [root, setRoot] = React.useState<HTMLElement | null>(null); const [root, setRoot] = React.useState<HTMLElement | null>(null);
@ -54,13 +53,22 @@ export const ConfirmationModal = React.memo(
[onClose] [onClose]
); );
const handleKeyCancel = React.useCallback(
(e: React.KeyboardEvent) => {
if (e.target === e.currentTarget && e.keyCode === 27) {
onClose();
}
},
[onClose]
);
return root return root
? createPortal( ? createPortal(
<div <div
// Not really a button. Just a background which can be clicked to close modal role="presentation"
role="button"
className="module-confirmation-dialog__overlay" className="module-confirmation-dialog__overlay"
onClick={handleCancel} onClick={handleCancel}
onKeyUp={handleKeyCancel}
> >
<ConfirmationDialog i18n={i18n} {...rest} onClose={onClose}> <ConfirmationDialog i18n={i18n} {...rest} onClose={onClose}>
{children} {children}

View file

@ -4,12 +4,8 @@ import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { gifUrl } from '../storybook/Fixtures'; import { gifUrl } from '../storybook/Fixtures';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore import enMessages from '../../_locales/en/messages.json';
import enMessages from '../../\_locales/en/messages.json';
import { ContactListItem } from './ContactListItem'; import { ContactListItem } from './ContactListItem';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -23,7 +23,7 @@ interface Props {
} }
export class ContactListItem extends React.Component<Props> { export class ContactListItem extends React.Component<Props> {
public renderAvatar() { public renderAvatar(): JSX.Element {
const { const {
avatarPath, avatarPath,
i18n, i18n,
@ -49,7 +49,7 @@ export class ContactListItem extends React.Component<Props> {
); );
} }
public render() { public render(): JSX.Element {
const { const {
i18n, i18n,
isAdmin, isAdmin,
@ -75,6 +75,7 @@ export class ContactListItem extends React.Component<Props> {
'module-contact-list-item', 'module-contact-list-item',
onClick ? 'module-contact-list-item--with-click-handler' : null onClick ? 'module-contact-list-item--with-click-handler' : null
)} )}
type="button"
> >
{this.renderAvatar()} {this.renderAvatar()}
<div className="module-contact-list-item__text"> <div className="module-contact-list-item__text">

View file

@ -1,22 +1,17 @@
import * as React from 'react'; import * as React from 'react';
import 'draft-js/dist/Draft.css';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { boolean, date, select, text } from '@storybook/addon-knobs';
import { import {
ConversationListItem, ConversationListItem,
MessageStatuses, MessageStatuses,
Props, Props,
} from './ConversationListItem'; } from './ConversationListItem';
// tslint:disable-next-line
import 'draft-js/dist/Draft.css';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { action } from '@storybook/addon-actions';
import { boolean, date, select, text } from '@storybook/addon-knobs';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -191,7 +186,6 @@ Line 4, well.`,
return messages.map(message => { return messages.map(message => {
const props = createProps({ const props = createProps({
name,
lastMessage: { lastMessage: {
text: message, text: message,
status: 'read', status: 'read',
@ -212,7 +206,6 @@ story.add('Various Times', () => {
return times.map(([lastUpdated, messageText]) => { return times.map(([lastUpdated, messageText]) => {
const props = createProps({ const props = createProps({
name,
lastUpdated, lastUpdated,
lastMessage: { lastMessage: {
text: messageText, text: messageText,
@ -227,12 +220,14 @@ story.add('Various Times', () => {
story.add('Missing Date', () => { story.add('Missing Date', () => {
const props = createProps(); const props = createProps();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return <ConversationListItem {...props} lastUpdated={undefined as any} />; return <ConversationListItem {...props} lastUpdated={undefined as any} />;
}); });
story.add('Missing Message', () => { story.add('Missing Message', () => {
const props = createProps(); const props = createProps();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return <ConversationListItem {...props} lastMessage={undefined as any} />; return <ConversationListItem {...props} lastMessage={undefined as any} />;
}); });
@ -242,6 +237,7 @@ story.add('Missing Text', () => {
return ( return (
<ConversationListItem <ConversationListItem
{...props} {...props}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
lastMessage={{ text: undefined as any, status: 'sent' }} lastMessage={{ text: undefined as any, status: 'sent' }}
/> />
); );

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { CSSProperties } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
@ -43,7 +43,7 @@ export type PropsData = {
draftPreview?: string; draftPreview?: string;
shouldShowDraft?: boolean; shouldShowDraft?: boolean;
typingContact?: Object; typingContact?: unknown;
lastMessage?: { lastMessage?: {
status: MessageStatusType; status: MessageStatusType;
text: string; text: string;
@ -53,14 +53,14 @@ export type PropsData = {
type PropsHousekeeping = { type PropsHousekeeping = {
i18n: LocalizerType; i18n: LocalizerType;
style?: Object; style?: CSSProperties;
onClick?: (id: string) => void; onClick?: (id: string) => void;
}; };
export type Props = PropsData & PropsHousekeeping; export type Props = PropsData & PropsHousekeeping;
export class ConversationListItem extends React.PureComponent<Props> { export class ConversationListItem extends React.PureComponent<Props> {
public renderAvatar() { public renderAvatar(): JSX.Element {
const { const {
avatarPath, avatarPath,
color, color,
@ -92,7 +92,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
); );
} }
public renderUnread() { public renderUnread(): JSX.Element | null {
const { unreadCount } = this.props; const { unreadCount } = this.props;
if (isNumber(unreadCount) && unreadCount > 0) { if (isNumber(unreadCount) && unreadCount > 0) {
@ -106,7 +106,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
return null; return null;
} }
public renderHeader() { public renderHeader(): JSX.Element {
const { const {
unreadCount, unreadCount,
i18n, i18n,
@ -162,7 +162,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
); );
} }
public renderMessage() { public renderMessage(): JSX.Element | null {
const { const {
draftPreview, draftPreview,
i18n, i18n,
@ -185,6 +185,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
// Note: instead of re-using showingDraft here we explode it because // Note: instead of re-using showingDraft here we explode it because
// typescript can't tell that draftPreview is truthy otherwise // typescript can't tell that draftPreview is truthy otherwise
// Avoiding touching logic to fix linting
/* eslint-disable no-nested-ternary */
const text = const text =
shouldShowDraft && draftPreview shouldShowDraft && draftPreview
? draftPreview ? draftPreview
@ -225,8 +227,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
) : null} ) : null}
<MessageBody <MessageBody
text={text.split('\n')[0]} text={text.split('\n')[0]}
disableJumbomoji={true} disableJumbomoji
disableLinks={true} disableLinks
i18n={i18n} i18n={i18n}
/> />
</> </>
@ -243,13 +245,15 @@ export class ConversationListItem extends React.PureComponent<Props> {
</div> </div>
); );
} }
/* eslint-enable no-nested-ternary */
public render() { public render(): JSX.Element {
const { unreadCount, onClick, id, isSelected, style } = this.props; const { unreadCount, onClick, id, isSelected, style } = this.props;
const withUnread = isNumber(unreadCount) && unreadCount > 0; const withUnread = isNumber(unreadCount) && unreadCount > 0;
return ( return (
<button <button
type="button"
onClick={() => { onClick={() => {
if (onClick) { if (onClick) {
onClick(id); onClick(id);

View file

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
// import classNames from 'classnames';
export interface Props { export interface Props {
duration: number; duration: number;
@ -24,19 +23,19 @@ export class Countdown extends React.Component<Props, State> {
this.state = { ratio }; this.state = { ratio };
} }
public componentDidMount() { public componentDidMount(): void {
this.startLoop(); this.startLoop();
} }
public componentDidUpdate() { public componentDidUpdate(): void {
this.startLoop(); this.startLoop();
} }
public componentWillUnmount() { public componentWillUnmount(): void {
this.stopLoop(); this.stopLoop();
} }
public startLoop() { public startLoop(): void {
if (this.looping) { if (this.looping) {
return; return;
} }
@ -45,11 +44,11 @@ export class Countdown extends React.Component<Props, State> {
requestAnimationFrame(this.loop); requestAnimationFrame(this.loop);
} }
public stopLoop() { public stopLoop(): void {
this.looping = false; this.looping = false;
} }
public loop = () => { public loop = (): void => {
const { onComplete, duration, expiresAt } = this.props; const { onComplete, duration, expiresAt } = this.props;
if (!this.looping) { if (!this.looping) {
return; return;
@ -68,7 +67,7 @@ export class Countdown extends React.Component<Props, State> {
} }
}; };
public render() { public render(): JSX.Element {
const { ratio } = this.state; const { ratio } = this.state;
const strokeDashoffset = ratio * CIRCUMFERENCE; const strokeDashoffset = ratio * CIRCUMFERENCE;

View file

@ -1,14 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { ExpiredBuildDialog } from './ExpiredBuildDialog';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs'; import { boolean } from '@storybook/addon-knobs';
import { ExpiredBuildDialog } from './ExpiredBuildDialog';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
storiesOf('Components/ExpiredBuildDialog', module).add( storiesOf('Components/ExpiredBuildDialog', module).add(

View file

@ -26,7 +26,9 @@ export const ExpiredBuildDialog = ({
tabIndex={-1} tabIndex={-1}
target="_blank" target="_blank"
> >
<button className="upgrade">{i18n('upgrade')}</button> <button type="button" className="upgrade">
{i18n('upgrade')}
</button>
</a> </a>
</div> </div>
</div> </div>

View file

@ -2,11 +2,8 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore import enMessages from '../../_locales/en/messages.json';
import enMessages from '../../\_locales/en/messages.json';
import { InContactsIcon } from './InContactsIcon'; import { InContactsIcon } from './InContactsIcon';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -10,6 +10,7 @@ type PropsType = {
export const InContactsIcon = (props: PropsType): JSX.Element => { export const InContactsIcon = (props: PropsType): JSX.Element => {
const { i18n } = props; const { i18n } = props;
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
return ( return (
<Tooltip <Tooltip
tagName="span" tagName="span"
@ -28,4 +29,5 @@ export const InContactsIcon = (props: PropsType): JSX.Element => {
/> />
</Tooltip> </Tooltip>
); );
/* eslint-enable jsx-a11y/no-noninteractive-tabindex */
}; };

View file

@ -1,16 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { IncomingCallBar } from './IncomingCallBar';
import { Colors, ColorType } from '../types/Colors';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
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 { IncomingCallBar } from './IncomingCallBar';
import { Colors, ColorType } from '../types/Colors';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const defaultProps = { const defaultProps = {

View file

@ -34,6 +34,7 @@ const CallButton = ({
className={`module-incoming-call__icon module-incoming-call__button--${classSuffix}`} className={`module-incoming-call__icon module-incoming-call__button--${classSuffix}`}
onClick={onClick} onClick={onClick}
tabIndex={tabIndex} tabIndex={tabIndex}
type="button"
> >
<Tooltip <Tooltip
arrowSize={6} arrowSize={6}
@ -48,7 +49,6 @@ const CallButton = ({
); );
}; };
// tslint:disable-next-line max-func-body-length
export const IncomingCallBar = ({ export const IncomingCallBar = ({
acceptCall, acceptCall,
callDetails, callDetails,

View file

@ -4,10 +4,7 @@ import { text } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { Intl, Props } from './Intl'; import { Intl, Props } from './Intl';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -40,7 +37,11 @@ story.add('Single String Replacement', () => {
story.add('Single Tag Replacement', () => { story.add('Single Tag Replacement', () => {
const props = createProps({ const props = createProps({
id: 'leftTheGroup', id: 'leftTheGroup',
components: [<button key="a-button">Theodora</button>], components: [
<button type="button" key="a-button">
Theodora
</button>,
],
}); });
return <Intl {...props} />; return <Intl {...props} />;

View file

@ -24,25 +24,23 @@ export class Intl extends React.Component<Props> {
index: number, index: number,
placeholderName: string, placeholderName: string,
key: number key: number
): FullJSXType | undefined { ): FullJSXType | null {
const { id, components } = this.props; const { id, components } = this.props;
if (!components) { if (!components) {
// tslint:disable-next-line no-console window.log.error(
console.log(
`Error: Intl component prop not provided; Metadata: id '${id}', index ${index}, placeholder '${placeholderName}'` `Error: Intl component prop not provided; Metadata: id '${id}', index ${index}, placeholder '${placeholderName}'`
); );
return; return null;
} }
if (Array.isArray(components)) { if (Array.isArray(components)) {
if (!components || !components.length || components.length <= index) { if (!components || !components.length || components.length <= index) {
// tslint:disable-next-line no-console window.log.error(
console.log(
`Error: Intl missing provided component for id '${id}', index ${index}` `Error: Intl missing provided component for id '${id}', index ${index}`
); );
return; return null;
} }
return <React.Fragment key={key}>{components[index]}</React.Fragment>; return <React.Fragment key={key}>{components[index]}</React.Fragment>;
@ -50,28 +48,30 @@ export class Intl extends React.Component<Props> {
const value = components[placeholderName]; const value = components[placeholderName];
if (!value) { if (!value) {
// tslint:disable-next-line no-console window.log.error(
console.log(
`Error: Intl missing provided component for id '${id}', placeholder '${placeholderName}'` `Error: Intl missing provided component for id '${id}', placeholder '${placeholderName}'`
); );
return; return null;
} }
return <React.Fragment key={key}>{value}</React.Fragment>; return <React.Fragment key={key}>{value}</React.Fragment>;
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public render() { public render() {
const { components, id, i18n, renderText } = this.props; const { components, id, i18n, renderText } = this.props;
const text = i18n(id); const text = i18n(id);
const results: Array<any> = []; const results: Array<
string | JSX.Element | Array<string | JSX.Element> | null
> = [];
const FIND_REPLACEMENTS = /\$([^$]+)\$/g; const FIND_REPLACEMENTS = /\$([^$]+)\$/g;
// We have to do this, because renderText is not required in our Props object, // We have to do this, because renderText is not required in our Props object,
// but it is always provided via defaultProps. // but it is always provided via defaultProps.
if (!renderText) { if (!renderText) {
return; return null;
} }
if (Array.isArray(components) && components.length > 1) { if (Array.isArray(components) && components.length > 1) {
@ -92,7 +92,7 @@ export class Intl extends React.Component<Props> {
while (match) { while (match) {
if (lastTextIndex < match.index) { if (lastTextIndex < match.index) {
const textWithNoReplacements = text.slice(lastTextIndex, match.index); const textWithNoReplacements = text.slice(lastTextIndex, match.index);
results.push(renderText({ text: textWithNoReplacements, key: key })); results.push(renderText({ text: textWithNoReplacements, key }));
key += 1; key += 1;
} }
@ -101,13 +101,12 @@ export class Intl extends React.Component<Props> {
componentIndex += 1; componentIndex += 1;
key += 1; key += 1;
// @ts-ignore
lastTextIndex = FIND_REPLACEMENTS.lastIndex; lastTextIndex = FIND_REPLACEMENTS.lastIndex;
match = FIND_REPLACEMENTS.exec(text); match = FIND_REPLACEMENTS.exec(text);
} }
if (lastTextIndex < text.length) { if (lastTextIndex < text.length) {
results.push(renderText({ text: text.slice(lastTextIndex), key: key })); results.push(renderText({ text: text.slice(lastTextIndex), key }));
key += 1; key += 1;
} }

View file

@ -6,11 +6,9 @@ import { storiesOf } from '@storybook/react';
import { LeftPane, PropsType } from './LeftPane'; import { LeftPane, PropsType } from './LeftPane';
import { PropsData } from './ConversationListItem'; import { PropsData } from './ConversationListItem';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/LeftPane', module); const story = storiesOf('Components/LeftPane', module);

View file

@ -1,5 +1,5 @@
import Measure, { BoundingRect, MeasuredComponentProps } from 'react-measure'; import Measure, { BoundingRect, MeasuredComponentProps } from 'react-measure';
import React from 'react'; import React, { CSSProperties } from 'react';
import { List } from 'react-virtualized'; import { List } from 'react-virtualized';
import { debounce, get } from 'lodash'; import { debounce, get } from 'lodash';
@ -47,14 +47,17 @@ type RowRendererParamsType = {
isScrolling: boolean; isScrolling: boolean;
isVisible: boolean; isVisible: boolean;
key: string; key: string;
parent: Object; parent: Record<string, unknown>;
style: Object; style: CSSProperties;
}; };
export class LeftPane extends React.Component<PropsType> { export class LeftPane extends React.Component<PropsType> {
public listRef = React.createRef<any>(); public listRef = React.createRef<List>();
public containerRef = React.createRef<HTMLDivElement>(); public containerRef = React.createRef<HTMLDivElement>();
public setFocusToFirstNeeded = false; public setFocusToFirstNeeded = false;
public setFocusToLastNeeded = false; public setFocusToLastNeeded = false;
public renderRow = ({ public renderRow = ({
@ -103,7 +106,7 @@ export class LeftPane extends React.Component<PropsType> {
style, style,
}: { }: {
key: string; key: string;
style: Object; style: CSSProperties;
}): JSX.Element => { }): JSX.Element => {
const { const {
archivedConversations, archivedConversations,
@ -123,6 +126,7 @@ export class LeftPane extends React.Component<PropsType> {
className="module-left-pane__archived-button" className="module-left-pane__archived-button"
style={style} style={style}
onClick={showArchivedConversations} onClick={showArchivedConversations}
type="button"
> >
{i18n('archivedConversations')}{' '} {i18n('archivedConversations')}{' '}
<span className="module-left-pane__archived-button__archived-count"> <span className="module-left-pane__archived-button__archived-count">
@ -132,7 +136,7 @@ export class LeftPane extends React.Component<PropsType> {
); );
}; };
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => { public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
const commandKey = get(window, 'platform') === 'darwin' && event.metaKey; const commandKey = get(window, 'platform') === 'darwin' && event.metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey; const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey;
const commandOrCtrl = commandKey || controlKey; const commandOrCtrl = commandKey || controlKey;
@ -154,12 +158,10 @@ export class LeftPane extends React.Component<PropsType> {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return;
} }
}; };
public handleFocus = () => { public handleFocus = (): void => {
const { selectedConversationId } = this.props; const { selectedConversationId } = this.props;
const { current: container } = this.containerRef; const { current: container } = this.containerRef;
@ -174,10 +176,9 @@ export class LeftPane extends React.Component<PropsType> {
/["\\]/g, /["\\]/g,
'\\$&' '\\$&'
); );
// tslint:disable-next-line no-unnecessary-type-assertion const target: HTMLElement | null = scrollingContainer.querySelector(
const target = scrollingContainer.querySelector(
`.module-conversation-list-item[data-id="${escapedId}"]` `.module-conversation-list-item[data-id="${escapedId}"]`
) as any; );
if (target && target.focus) { if (target && target.focus) {
target.focus(); target.focus();
@ -190,7 +191,7 @@ export class LeftPane extends React.Component<PropsType> {
} }
}; };
public scrollToRow = (row: number) => { public scrollToRow = (row: number): void => {
if (!this.listRef || !this.listRef.current) { if (!this.listRef || !this.listRef.current) {
return; return;
} }
@ -198,40 +199,39 @@ export class LeftPane extends React.Component<PropsType> {
this.listRef.current.scrollToRow(row); this.listRef.current.scrollToRow(row);
}; };
public getScrollContainer = () => { public getScrollContainer = (): HTMLDivElement | null => {
if (!this.listRef || !this.listRef.current) { if (!this.listRef || !this.listRef.current) {
return; return null;
} }
const list = this.listRef.current; const list = this.listRef.current;
if (!list.Grid || !list.Grid._scrollingContainer) { // TODO: DESKTOP-689
return; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const grid: any = list.Grid;
if (!grid || !grid._scrollingContainer) {
return null;
} }
return list.Grid._scrollingContainer as HTMLDivElement; return grid._scrollingContainer as HTMLDivElement;
}; };
public setFocusToFirst = () => { public setFocusToFirst = (): void => {
const scrollContainer = this.getScrollContainer(); const scrollContainer = this.getScrollContainer();
if (!scrollContainer) { if (!scrollContainer) {
return; return;
} }
// tslint:disable-next-line no-unnecessary-type-assertion const item: HTMLElement | null = scrollContainer.querySelector(
const item = scrollContainer.querySelector(
'.module-conversation-list-item' '.module-conversation-list-item'
) as any; );
if (item && item.focus) { if (item && item.focus) {
item.focus(); item.focus();
return;
} }
}; };
// tslint:disable-next-line member-ordering
public onScroll = debounce( public onScroll = debounce(
() => { (): void => {
if (this.setFocusToFirstNeeded) { if (this.setFocusToFirstNeeded) {
this.setFocusToFirstNeeded = false; this.setFocusToFirstNeeded = false;
this.setFocusToFirst(); this.setFocusToFirst();
@ -244,26 +244,22 @@ export class LeftPane extends React.Component<PropsType> {
return; return;
} }
// tslint:disable-next-line no-unnecessary-type-assertion const button: HTMLElement | null = scrollContainer.querySelector(
const button = scrollContainer.querySelector(
'.module-left-pane__archived-button' '.module-left-pane__archived-button'
) as any; );
if (button && button.focus) { if (button && button.focus) {
button.focus(); button.focus();
return; return;
} }
// tslint:disable-next-line no-unnecessary-type-assertion const items: NodeListOf<HTMLElement> = scrollContainer.querySelectorAll(
const items = scrollContainer.querySelectorAll(
'.module-conversation-list-item' '.module-conversation-list-item'
) as any; );
if (items && items.length > 0) { if (items && items.length > 0) {
const last = items[items.length - 1]; const last = items[items.length - 1];
if (last && last.focus) { if (last && last.focus) {
last.focus(); last.focus();
return;
} }
} }
} }
@ -272,7 +268,7 @@ export class LeftPane extends React.Component<PropsType> {
{ maxWait: 100 } { maxWait: 100 }
); );
public getLength = () => { public getLength = (): number => {
const { archivedConversations, conversations, showArchived } = this.props; const { archivedConversations, conversations, showArchived } = this.props;
if (!conversations || !archivedConversations) { if (!conversations || !archivedConversations) {
@ -339,7 +335,7 @@ export class LeftPane extends React.Component<PropsType> {
onFocus={this.handleFocus} onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
ref={this.containerRef} ref={this.containerRef}
role="group" role="presentation"
tabIndex={-1} tabIndex={-1}
> >
<List <List
@ -367,6 +363,8 @@ export class LeftPane extends React.Component<PropsType> {
onClick={showInbox} onClick={showInbox}
className="module-left-pane__to-inbox-button" className="module-left-pane__to-inbox-button"
title={i18n('backToInbox')} title={i18n('backToInbox')}
aria-label={i18n('backToInbox')}
type="button"
/> />
<div className="module-left-pane__archive-header-text"> <div className="module-left-pane__archive-header-text">
{i18n('archivedConversations')} {i18n('archivedConversations')}
@ -386,7 +384,8 @@ export class LeftPane extends React.Component<PropsType> {
showArchived, showArchived,
} = this.props; } = this.props;
/* tslint:disable no-non-null-assertion */ // Relying on 3rd party code for contentRect.bounds
/* eslint-disable @typescript-eslint/no-non-null-assertion */
return ( return (
<div className="module-left-pane"> <div className="module-left-pane">
<div className="module-left-pane__header"> <div className="module-left-pane__header">
@ -401,7 +400,7 @@ export class LeftPane extends React.Component<PropsType> {
{i18n('archiveHelperText')} {i18n('archiveHelperText')}
</div> </div>
)} )}
<Measure bounds={true}> <Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => ( {({ contentRect, measureRef }: MeasuredComponentProps) => (
<div className="module-left-pane__list--measure" ref={measureRef}> <div className="module-left-pane__list--measure" ref={measureRef}>
<div className="module-left-pane__list--wrapper"> <div className="module-left-pane__list--wrapper">

View file

@ -12,11 +12,9 @@ import {
VIDEO_MP4, VIDEO_MP4,
VIDEO_QUICKTIME, VIDEO_QUICKTIME,
} from '../types/MIME'; } from '../types/MIME';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Lightbox', module); const story = storiesOf('Components/Lightbox', module);

View file

@ -1,5 +1,3 @@
// tslint:disable:react-a11y-anchors
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
@ -52,6 +50,14 @@ const styles = {
bottom: 0, bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.9)', backgroundColor: 'rgba(0, 0, 0, 0.9)',
} as React.CSSProperties, } as React.CSSProperties,
buttonContainer: {
backgroundColor: 'transparent',
border: 'none',
display: 'flex',
flexDirection: 'column',
outline: 'none',
padding: 0,
} as React.CSSProperties,
mainContainer: { mainContainer: {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
@ -129,7 +135,7 @@ const styles = {
letterSpacing: '0px', letterSpacing: '0px',
lineHeight: '18px', lineHeight: '18px',
// This cast is necessary or typescript chokes // This cast is necessary or typescript chokes
textAlign: 'center' as 'center', textAlign: 'center' as const,
padding: '6px', padding: '6px',
paddingLeft: '18px', paddingLeft: '18px',
paddingRight: '18px', paddingRight: '18px',
@ -137,12 +143,13 @@ const styles = {
}; };
interface IconButtonProps { interface IconButtonProps {
i18n: LocalizerType;
onClick?: () => void; onClick?: () => void;
style?: React.CSSProperties; style?: React.CSSProperties;
type: 'save' | 'close' | 'previous' | 'next'; type: 'save' | 'close' | 'previous' | 'next';
} }
const IconButton = ({ onClick, style, type }: IconButtonProps) => { const IconButton = ({ i18n, onClick, style, type }: IconButtonProps) => {
const clickHandler = (event: React.MouseEvent<HTMLButtonElement>): void => { const clickHandler = (event: React.MouseEvent<HTMLButtonElement>): void => {
event.preventDefault(); event.preventDefault();
if (!onClick) { if (!onClick) {
@ -157,6 +164,8 @@ const IconButton = ({ onClick, style, type }: IconButtonProps) => {
onClick={clickHandler} onClick={clickHandler}
className={classNames('iconButton', type)} className={classNames('iconButton', type)}
style={style} style={style}
aria-label={i18n(type)}
type="button"
/> />
); );
}; };
@ -166,10 +175,12 @@ const IconButtonPlaceholder = () => (
); );
const Icon = ({ const Icon = ({
i18n,
onClick, onClick,
url, url,
}: { }: {
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; i18n: LocalizerType;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
url: string; url: string;
}) => ( }) => (
<button <button
@ -179,19 +190,22 @@ const Icon = ({
maxWidth: 200, maxWidth: 200,
}} }}
onClick={onClick} onClick={onClick}
aria-label={i18n('unsupportedAttachment')}
type="button"
/> />
); );
export class Lightbox extends React.Component<Props, State> { export class Lightbox extends React.Component<Props, State> {
public readonly containerRef = React.createRef<HTMLDivElement>(); public readonly containerRef = React.createRef<HTMLDivElement>();
public readonly videoRef = React.createRef<HTMLVideoElement>(); public readonly videoRef = React.createRef<HTMLVideoElement>();
public readonly focusRef = React.createRef<HTMLDivElement>(); public readonly focusRef = React.createRef<HTMLDivElement>();
public previousFocus: any;
public state: State = {}; public previousFocus: HTMLElement | null = null;
public componentDidMount() { public componentDidMount(): void {
this.previousFocus = document.activeElement; this.previousFocus = document.activeElement as HTMLElement;
const { isViewOnce } = this.props; const { isViewOnce } = this.props;
@ -214,7 +228,7 @@ export class Lightbox extends React.Component<Props, State> {
}); });
} }
public componentWillUnmount() { public componentWillUnmount(): void {
if (this.previousFocus && this.previousFocus.focus) { if (this.previousFocus && this.previousFocus.focus) {
this.previousFocus.focus(); this.previousFocus.focus();
} }
@ -230,34 +244,33 @@ export class Lightbox extends React.Component<Props, State> {
} }
} }
public getVideo() { public getVideo(): HTMLVideoElement | null {
if (!this.videoRef) { if (!this.videoRef) {
return; return null;
} }
const { current } = this.videoRef; const { current } = this.videoRef;
if (!current) { if (!current) {
return; return null;
} }
return current; return current;
} }
public playVideo() { public playVideo(): void {
const video = this.getVideo(); const video = this.getVideo();
if (!video) { if (!video) {
return; return;
} }
if (video.paused) { if (video.paused) {
// tslint:disable-next-line no-floating-promises
video.play(); video.play();
} else { } else {
video.pause(); video.pause();
} }
} }
public render() { public render(): JSX.Element {
const { const {
caption, caption,
contentType, contentType,
@ -275,8 +288,9 @@ export class Lightbox extends React.Component<Props, State> {
className="module-lightbox" className="module-lightbox"
style={styles.container} style={styles.container}
onClick={this.onContainerClick} onClick={this.onContainerClick}
onKeyUp={this.onContainerKeyUp}
ref={this.containerRef} ref={this.containerRef}
role="dialog" role="presentation"
> >
<div style={styles.mainContainer} tabIndex={-1} ref={this.focusRef}> <div style={styles.mainContainer} tabIndex={-1} ref={this.focusRef}>
<div style={styles.controlsOffsetPlaceholder} /> <div style={styles.controlsOffsetPlaceholder} />
@ -287,9 +301,10 @@ export class Lightbox extends React.Component<Props, State> {
{caption ? <div style={styles.caption}>{caption}</div> : null} {caption ? <div style={styles.caption}>{caption}</div> : null}
</div> </div>
<div style={styles.controls}> <div style={styles.controls}>
<IconButton type="close" onClick={this.onClose} /> <IconButton i18n={i18n} type="close" onClick={this.onClose} />
{onSave ? ( {onSave ? (
<IconButton <IconButton
i18n={i18n}
type="save" type="save"
onClick={onSave} onClick={onSave}
style={styles.saveButton} style={styles.saveButton}
@ -304,12 +319,12 @@ export class Lightbox extends React.Component<Props, State> {
) : ( ) : (
<div style={styles.navigationContainer}> <div style={styles.navigationContainer}>
{onPrevious ? ( {onPrevious ? (
<IconButton type="previous" onClick={onPrevious} /> <IconButton i18n={i18n} type="previous" onClick={onPrevious} />
) : ( ) : (
<IconButtonPlaceholder /> <IconButtonPlaceholder />
)} )}
{onNext ? ( {onNext ? (
<IconButton type="next" onClick={onNext} /> <IconButton i18n={i18n} type="next" onClick={onNext} />
) : ( ) : (
<IconButtonPlaceholder /> <IconButtonPlaceholder />
)} )}
@ -333,12 +348,17 @@ export class Lightbox extends React.Component<Props, State> {
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType); const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
if (isImageTypeSupported) { if (isImageTypeSupported) {
return ( return (
<img <button
alt={i18n('lightboxImageAlt')} type="button"
style={styles.object} style={styles.buttonContainer}
src={objectURL}
onClick={this.onObjectClick} onClick={this.onObjectClick}
/> >
<img
alt={i18n('lightboxImageAlt')}
style={styles.object}
src={objectURL}
/>
</button>
); );
} }
@ -366,13 +386,14 @@ export class Lightbox extends React.Component<Props, State> {
? 'images/movie.svg' ? 'images/movie.svg'
: 'images/image.svg'; : 'images/image.svg';
return <Icon url={iconUrl} onClick={this.onObjectClick} />; return <Icon i18n={i18n} url={iconUrl} onClick={this.onObjectClick} />;
} }
// tslint:disable-next-line no-console window.log.info('Lightbox: Unexpected content type', { contentType });
console.log('Lightbox: Unexpected content type', { contentType });
return <Icon onClick={this.onObjectClick} url="images/file.svg" />; return (
<Icon i18n={i18n} onClick={this.onObjectClick} url="images/file.svg" />
);
}; };
private readonly onClose = () => { private readonly onClose = () => {
@ -436,8 +457,21 @@ export class Lightbox extends React.Component<Props, State> {
this.onClose(); this.onClose();
}; };
private readonly onContainerKeyUp = (
event: React.KeyboardEvent<HTMLDivElement>
) => {
if (
(this.containerRef && event.target !== this.containerRef.current) ||
event.keyCode !== 27
) {
return;
}
this.onClose();
};
private readonly onObjectClick = ( private readonly onObjectClick = (
event: React.MouseEvent<HTMLButtonElement | HTMLImageElement> event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
) => { ) => {
event.stopPropagation(); event.stopPropagation();
this.onClose(); this.onClose();

View file

@ -1,16 +1,14 @@
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { LightboxGallery, Props } from './LightboxGallery';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { number } from '@storybook/addon-knobs'; import { number } from '@storybook/addon-knobs';
import { LightboxGallery, Props } from './LightboxGallery';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
import { IMAGE_JPEG, VIDEO_MP4 } from '../types/MIME'; import { IMAGE_JPEG, VIDEO_MP4 } from '../types/MIME';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/LightboxGallery', module); const story = storiesOf('Components/LightboxGallery', module);

View file

@ -1,6 +1,3 @@
/**
* @prettier
*/
import React from 'react'; import React from 'react';
import * as MIME from '../types/MIME'; import * as MIME from '../types/MIME';
@ -44,11 +41,11 @@ export class LightboxGallery extends React.Component<Props, State> {
super(props); super(props);
this.state = { this.state = {
selectedIndex: this.props.selectedIndex, selectedIndex: props.selectedIndex,
}; };
} }
public render() { public render(): JSX.Element {
const { close, media, onSave, i18n } = this.props; const { close, media, onSave, i18n } = this.props;
const { selectedIndex } = this.state; const { selectedIndex } = this.state;

View file

@ -3,11 +3,8 @@ import { storiesOf } from '@storybook/react';
import { text, withKnobs } from '@storybook/addon-knobs'; import { text, withKnobs } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { MainHeader, PropsType } from './MainHeader'; import { MainHeader, PropsType } from './MainHeader';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -19,6 +16,8 @@ const requiredText = (name: string, value: string | undefined) =>
const optionalText = (name: string, value: string | undefined) => const optionalText = (name: string, value: string | undefined) =>
text(name, value || '') || undefined; text(name, value || '') || undefined;
// Storybook types are incorrect
// eslint-disable-next-line @typescript-eslint/no-explicit-any
story.addDecorator((withKnobs as any)({ escapeHTML: false })); story.addDecorator((withKnobs as any)({ escapeHTML: false }));
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({

View file

@ -76,7 +76,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
}; };
} }
public componentDidUpdate(prevProps: PropsType) { public componentDidUpdate(prevProps: PropsType): void {
const { searchConversationId, startSearchCounter } = this.props; const { searchConversationId, startSearchCounter } = this.props;
// When user chooses to search in a given conversation we focus the field for them // When user chooses to search in a given conversation we focus the field for them
@ -92,7 +92,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
} }
} }
public handleOutsideClick = ({ target }: MouseEvent) => { public handleOutsideClick = ({ target }: MouseEvent): void => {
const { popperRoot, showingAvatarPopup } = this.state; const { popperRoot, showingAvatarPopup } = this.state;
if ( if (
@ -104,13 +104,13 @@ export class MainHeader extends React.Component<PropsType, StateType> {
} }
}; };
public handleOutsideKeyDown = (event: KeyboardEvent) => { public handleOutsideKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
this.hideAvatarPopup(); this.hideAvatarPopup();
} }
}; };
public showAvatarPopup = () => { public showAvatarPopup = (): void => {
const popperRoot = document.createElement('div'); const popperRoot = document.createElement('div');
document.body.appendChild(popperRoot); document.body.appendChild(popperRoot);
@ -122,7 +122,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
document.addEventListener('keydown', this.handleOutsideKeyDown); document.addEventListener('keydown', this.handleOutsideKeyDown);
}; };
public hideAvatarPopup = () => { public hideAvatarPopup = (): void => {
const { popperRoot } = this.state; const { popperRoot } = this.state;
document.removeEventListener('click', this.handleOutsideClick); document.removeEventListener('click', this.handleOutsideClick);
@ -138,7 +138,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
} }
}; };
public componentWillUnmount() { public componentWillUnmount(): void {
const { popperRoot } = this.state; const { popperRoot } = this.state;
document.removeEventListener('click', this.handleOutsideClick); document.removeEventListener('click', this.handleOutsideClick);
@ -149,8 +149,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
} }
} }
// tslint:disable-next-line member-ordering public search = debounce((searchTerm: string): void => {
public search = debounce((searchTerm: string) => {
const { const {
i18n, i18n,
ourConversationId, ourConversationId,
@ -179,7 +178,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
} }
}, 200); }, 200);
public updateSearch = (event: React.FormEvent<HTMLInputElement>) => { public updateSearch = (event: React.FormEvent<HTMLInputElement>): void => {
const { const {
updateSearchTerm, updateSearchTerm,
clearConversationSearch, clearConversationSearch,
@ -209,21 +208,23 @@ export class MainHeader extends React.Component<PropsType, StateType> {
this.search(searchTerm); this.search(searchTerm);
}; };
public clearSearch = () => { public clearSearch = (): void => {
const { clearSearch } = this.props; const { clearSearch } = this.props;
clearSearch(); clearSearch();
this.setFocus(); this.setFocus();
}; };
public clearConversationSearch = () => { public clearConversationSearch = (): void => {
const { clearConversationSearch } = this.props; const { clearConversationSearch } = this.props;
clearConversationSearch(); clearConversationSearch();
this.setFocus(); this.setFocus();
}; };
public handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { public handleKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>
): void => {
const { const {
clearConversationSearch, clearConversationSearch,
clearSearch, clearSearch,
@ -258,7 +259,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
event.stopPropagation(); event.stopPropagation();
}; };
public handleXButton = () => { public handleXButton = (): void => {
const { const {
searchConversationId, searchConversationId,
clearConversationSearch, clearConversationSearch,
@ -274,22 +275,19 @@ export class MainHeader extends React.Component<PropsType, StateType> {
this.setFocus(); this.setFocus();
}; };
public setFocus = () => { public setFocus = (): void => {
if (this.inputRef.current) { if (this.inputRef.current) {
// @ts-ignore
this.inputRef.current.focus(); this.inputRef.current.focus();
} }
}; };
public setSelected = () => { public setSelected = (): void => {
if (this.inputRef.current) { if (this.inputRef.current) {
// @ts-ignore
this.inputRef.current.select(); this.inputRef.current.select();
} }
}; };
// tslint:disable-next-line:max-func-body-length public render(): JSX.Element {
public render() {
const { const {
avatarPath, avatarPath,
color, color,
@ -366,6 +364,8 @@ export class MainHeader extends React.Component<PropsType, StateType> {
className="module-main-header__search__in-conversation-pill" className="module-main-header__search__in-conversation-pill"
onClick={this.clearSearch} onClick={this.clearSearch}
tabIndex={-1} tabIndex={-1}
type="button"
aria-label={i18n('clearSearch')}
> >
<div className="module-main-header__search__in-conversation-pill__avatar-container"> <div className="module-main-header__search__in-conversation-pill__avatar-container">
<div className="module-main-header__search__in-conversation-pill__avatar" /> <div className="module-main-header__search__in-conversation-pill__avatar" />
@ -377,6 +377,8 @@ export class MainHeader extends React.Component<PropsType, StateType> {
className="module-main-header__search__icon" className="module-main-header__search__icon"
onClick={this.setFocus} onClick={this.setFocus}
tabIndex={-1} tabIndex={-1}
type="button"
aria-label={i18n('search')}
/> />
)} )}
<input <input
@ -402,6 +404,8 @@ export class MainHeader extends React.Component<PropsType, StateType> {
tabIndex={-1} tabIndex={-1}
className="module-main-header__search__cancel-icon" className="module-main-header__search__cancel-icon"
onClick={this.handleXButton} onClick={this.handleXButton}
type="button"
aria-label={i18n('cancel')}
/> />
) : null} ) : null}
</div> </div>

View file

@ -2,17 +2,16 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text, withKnobs } from '@storybook/addon-knobs'; import { text, withKnobs } from '@storybook/addon-knobs';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { MessageBodyHighlight, Props } from './MessageBodyHighlight'; import { MessageBodyHighlight, Props } from './MessageBodyHighlight';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/MessageBodyHighlight', module); const story = storiesOf('Components/MessageBodyHighlight', module);
// Storybook types are incorrect
// eslint-disable-next-line @typescript-eslint/no-explicit-any
story.addDecorator((withKnobs as any)({ escapeHTML: false })); story.addDecorator((withKnobs as any)({ escapeHTML: false }));
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const createProps = (overrideProps: Partial<Props> = {}): Props => ({

View file

@ -38,9 +38,9 @@ const renderEmoji = ({
); );
export class MessageBodyHighlight extends React.Component<Props> { export class MessageBodyHighlight extends React.Component<Props> {
public render() { public render(): JSX.Element | Array<JSX.Element> {
const { text, i18n } = this.props; const { text, i18n } = this.props;
const results: Array<any> = []; const results: Array<JSX.Element> = [];
const FIND_BEGIN_END = /<<left>>(.+?)<<right>>/g; const FIND_BEGIN_END = /<<left>>(.+?)<<right>>/g;
let match = FIND_BEGIN_END.exec(text); let match = FIND_BEGIN_END.exec(text);
@ -49,12 +49,7 @@ export class MessageBodyHighlight extends React.Component<Props> {
if (!match) { if (!match) {
return ( return (
<MessageBody <MessageBody disableJumbomoji disableLinks text={text} i18n={i18n} />
disableJumbomoji={true}
disableLinks={true}
text={text}
i18n={i18n}
/>
); );
} }
@ -63,11 +58,12 @@ export class MessageBodyHighlight extends React.Component<Props> {
while (match) { while (match) {
if (last < match.index) { if (last < match.index) {
const beforeText = text.slice(last, match.index); const beforeText = text.slice(last, match.index);
count += 1;
results.push( results.push(
renderEmoji({ renderEmoji({
text: beforeText, text: beforeText,
sizeClass, sizeClass,
key: count++, key: count,
i18n, i18n,
renderNonEmoji: renderNewLines, renderNonEmoji: renderNewLines,
}) })
@ -75,29 +71,30 @@ export class MessageBodyHighlight extends React.Component<Props> {
} }
const [, toHighlight] = match; const [, toHighlight] = match;
count += 2;
results.push( results.push(
<span className="module-message-body__highlight" key={count++}> <span className="module-message-body__highlight" key={count - 1}>
{renderEmoji({ {renderEmoji({
text: toHighlight, text: toHighlight,
sizeClass, sizeClass,
key: count++, key: count,
i18n, i18n,
renderNonEmoji: renderNewLines, renderNonEmoji: renderNewLines,
})} })}
</span> </span>
); );
// @ts-ignore
last = FIND_BEGIN_END.lastIndex; last = FIND_BEGIN_END.lastIndex;
match = FIND_BEGIN_END.exec(text); match = FIND_BEGIN_END.exec(text);
} }
if (last < text.length) { if (last < text.length) {
count += 1;
results.push( results.push(
renderEmoji({ renderEmoji({
text: text.slice(last), text: text.slice(last),
sizeClass, sizeClass,
key: count++, key: count,
i18n, i18n,
renderNonEmoji: renderNewLines, renderNonEmoji: renderNewLines,
}) })

View file

@ -1,18 +1,17 @@
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { MessageSearchResult, PropsType } from './MessageSearchResult';
import { boolean, text, withKnobs } from '@storybook/addon-knobs'; import { boolean, text, withKnobs } from '@storybook/addon-knobs';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
import { MessageSearchResult, PropsType } from './MessageSearchResult';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/MessageSearchResult', module); const story = storiesOf('Components/MessageSearchResult', module);
// Storybook types are incorrect
// eslint-disable-next-line @typescript-eslint/no-explicit-any
story.addDecorator((withKnobs as any)({ escapeHTML: false })); story.addDecorator((withKnobs as any)({ escapeHTML: false }));
const someone = { const someone = {
@ -41,8 +40,8 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
'snippet', 'snippet',
overrideProps.snippet || "What's <<left>>going<<right>> on?" overrideProps.snippet || "What's <<left>>going<<right>> on?"
), ),
from: overrideProps.from as any, from: overrideProps.from as PropsType['from'],
to: overrideProps.to as any, to: overrideProps.to as PropsType['to'],
isSelected: boolean('isSelected', overrideProps.isSelected || false), isSelected: boolean('isSelected', overrideProps.isSelected || false),
openConversationInternal: action('openConversationInternal'), openConversationInternal: action('openConversationInternal'),
isSearchingInConversation: boolean( isSearchingInConversation: boolean(

View file

@ -50,7 +50,7 @@ type PropsHousekeepingType = {
export type PropsType = PropsDataType & PropsHousekeepingType; export type PropsType = PropsDataType & PropsHousekeepingType;
export class MessageSearchResult extends React.PureComponent<PropsType> { export class MessageSearchResult extends React.PureComponent<PropsType> {
public renderFromName() { public renderFromName(): JSX.Element {
const { from, i18n, to } = this.props; const { from, i18n, to } = this.props;
if (from.isMe && to.isMe) { if (from.isMe && to.isMe) {
@ -80,7 +80,7 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
); );
} }
public renderFrom() { public renderFrom(): JSX.Element {
const { i18n, to, isSearchingInConversation } = this.props; const { i18n, to, isSearchingInConversation } = this.props;
const fromName = this.renderFromName(); const fromName = this.renderFromName();
@ -108,7 +108,7 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
); );
} }
public renderAvatar() { public renderAvatar(): JSX.Element {
const { from, i18n, to } = this.props; const { from, i18n, to } = this.props;
const isNoteToSelf = from.isMe && to.isMe; const isNoteToSelf = from.isMe && to.isMe;
@ -118,7 +118,7 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
color={from.color} color={from.color}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
name={name} name={from.name}
noteToSelf={isNoteToSelf} noteToSelf={isNoteToSelf}
phoneNumber={from.phoneNumber} phoneNumber={from.phoneNumber}
profileName={from.profileName} profileName={from.profileName}
@ -128,7 +128,7 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
); );
} }
public render() { public render(): JSX.Element | null {
const { const {
from, from,
i18n, i18n,
@ -157,6 +157,7 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
isSelected ? 'module-message-search-result--is-selected' : null isSelected ? 'module-message-search-result--is-selected' : null
)} )}
data-id={id} data-id={id}
type="button"
> >
{this.renderAvatar()} {this.renderAvatar()}
<div className="module-message-search-result__text"> <div className="module-message-search-result__text">

View file

@ -1,15 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { NetworkStatus } from './NetworkStatus';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean, select } from '@storybook/addon-knobs'; import { boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { NetworkStatus } from './NetworkStatus';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const defaultProps = { const defaultProps = {

View file

@ -40,13 +40,13 @@ export const NetworkStatus = ({
socketStatus, socketStatus,
manualReconnect, manualReconnect,
}: PropsType): JSX.Element | null => { }: PropsType): JSX.Element | null => {
if (!hasNetworkDialog) {
return null;
}
const [isConnecting, setIsConnecting] = React.useState<boolean>(false); const [isConnecting, setIsConnecting] = React.useState<boolean>(false);
React.useEffect(() => { React.useEffect(() => {
let timeout: any; if (!hasNetworkDialog) {
return () => null;
}
let timeout: NodeJS.Timeout;
if (isConnecting) { if (isConnecting) {
timeout = setTimeout(() => { timeout = setTimeout(() => {
@ -59,7 +59,11 @@ export const NetworkStatus = ({
clearTimeout(timeout); clearTimeout(timeout);
} }
}; };
}, [isConnecting, setIsConnecting]); }, [hasNetworkDialog, isConnecting, setIsConnecting]);
if (!hasNetworkDialog) {
return null;
}
const reconnect = () => { const reconnect = () => {
setIsConnecting(true); setIsConnecting(true);
@ -68,7 +72,9 @@ export const NetworkStatus = ({
const manualReconnectButton = (): JSX.Element => ( const manualReconnectButton = (): JSX.Element => (
<div className="module-left-pane-dialog__actions"> <div className="module-left-pane-dialog__actions">
<button onClick={reconnect}>{i18n('connect')}</button> <button onClick={reconnect} type="button">
{i18n('connect')}
</button>
</div> </div>
); );
@ -77,7 +83,8 @@ export const NetworkStatus = ({
subtext: i18n('connectingHangOn'), subtext: i18n('connectingHangOn'),
title: i18n('connecting'), title: i18n('connecting'),
}); });
} else if (!isOnline) { }
if (!isOnline) {
return renderDialog({ return renderDialog({
renderActionableButton: manualReconnectButton, renderActionableButton: manualReconnectButton,
subtext: i18n('checkNetworkConnection'), subtext: i18n('checkNetworkConnection'),

View file

@ -1,15 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { RelinkDialog } from './RelinkDialog';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs'; import { boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { RelinkDialog } from './RelinkDialog';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const defaultProps = { const defaultProps = {

View file

@ -24,7 +24,9 @@ export const RelinkDialog = ({
<span>{i18n('unlinkedWarning')}</span> <span>{i18n('unlinkedWarning')}</span>
</div> </div>
<div className="module-left-pane-dialog__actions"> <div className="module-left-pane-dialog__actions">
<button onClick={relinkDevice}>{i18n('relink')}</button> <button onClick={relinkDevice} type="button">
{i18n('relink')}
</button>
</div> </div>
</div> </div>
); );

View file

@ -1,15 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
import { ConversationType } from '../state/ducks/conversations';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
import { ConversationType } from '../state/ducks/conversations';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const contactWithAllData = { const contactWithAllData = {

View file

@ -39,7 +39,7 @@ const SafetyDialogContents = ({
if (cancelButtonRef && cancelButtonRef.current) { if (cancelButtonRef && cancelButtonRef.current) {
cancelButtonRef.current.focus(); cancelButtonRef.current.focus();
} }
}, [contacts]); }, [cancelButtonRef, contacts]);
return ( return (
<> <>
@ -88,6 +88,7 @@ const SafetyDialogContents = ({
onView(contact); onView(contact);
}} }}
tabIndex={0} tabIndex={0}
type="button"
> >
{i18n('view')} {i18n('view')}
</button> </button>
@ -101,6 +102,7 @@ const SafetyDialogContents = ({
onClick={onCancel} onClick={onCancel}
ref={cancelButtonRef} ref={cancelButtonRef}
tabIndex={0} tabIndex={0}
type="button"
> >
{i18n('cancel')} {i18n('cancel')}
</button> </button>
@ -108,6 +110,7 @@ const SafetyDialogContents = ({
className="module-sfn-dialog__actions--confirm" className="module-sfn-dialog__actions--confirm"
onClick={onConfirm} onClick={onConfirm}
tabIndex={0} tabIndex={0}
type="button"
> >
{confirmText || i18n('sendMessageToContact')} {confirmText || i18n('sendMessageToContact')}
</button> </button>

View file

@ -1,16 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { SafetyNumberViewer } from './SafetyNumberViewer';
import { ConversationType } from '../state/ducks/conversations';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { boolean, text } from '@storybook/addon-knobs'; import { boolean, text } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { SafetyNumberViewer } from './SafetyNumberViewer';
import { ConversationType } from '../state/ducks/conversations';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const contactWithAllData = { const contactWithAllData = {

View file

@ -25,14 +25,18 @@ export const SafetyNumberViewer = ({
toggleVerified, toggleVerified,
verificationDisabled, verificationDisabled,
}: SafetyNumberViewerProps): JSX.Element | null => { }: SafetyNumberViewerProps): JSX.Element | null => {
React.useEffect(() => {
if (!contact) {
return;
}
generateSafetyNumber(contact);
}, [contact, generateSafetyNumber, safetyNumber]);
if (!contact) { if (!contact) {
return null; return null;
} }
React.useEffect(() => {
generateSafetyNumber(contact);
}, [safetyNumber]);
const showNumber = Boolean(contact.name || contact.profileName); const showNumber = Boolean(contact.name || contact.profileName);
const numberFragment = showNumber ? ` · ${contact.phoneNumber}` : ''; const numberFragment = showNumber ? ` · ${contact.phoneNumber}` : '';
const name = `${contact.title}${numberFragment}`; const name = `${contact.title}${numberFragment}`;
@ -40,7 +44,7 @@ export const SafetyNumberViewer = ({
<span className="module-safety-number__bold-name">{name}</span> <span className="module-safety-number__bold-name">{name}</span>
); );
const isVerified = contact.isVerified; const { isVerified } = contact;
const verifiedStatusKey = isVerified ? 'isVerified' : 'isNotVerified'; const verifiedStatusKey = isVerified ? 'isVerified' : 'isNotVerified';
const safetyNumberChangedKey = safetyNumberChanged const safetyNumberChangedKey = safetyNumberChanged
? 'changedRightAfterVerify' ? 'changedRightAfterVerify'
@ -51,7 +55,7 @@ export const SafetyNumberViewer = ({
<div className="module-safety-number"> <div className="module-safety-number">
{onClose && ( {onClose && (
<div className="module-safety-number__close-button"> <div className="module-safety-number__close-button">
<button onClick={onClose} tabIndex={0}> <button onClick={onClose} tabIndex={0} type="button">
<span /> <span />
</button> </button>
</div> </div>
@ -86,6 +90,7 @@ export const SafetyNumberViewer = ({
toggleVerified(contact); toggleVerified(contact);
}} }}
tabIndex={0} tabIndex={0}
type="button"
> >
{verifyButtonText} {verifyButtonText}
</button> </button>

View file

@ -1,19 +1,14 @@
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { SearchResults } from './SearchResults'; import { SearchResults } from './SearchResults';
import { import {
MessageSearchResult, MessageSearchResult,
PropsDataType as MessageSearchResultPropsType, PropsDataType as MessageSearchResultPropsType,
} from './MessageSearchResult'; } from './MessageSearchResult';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react';
//import { boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { import {
gifUrl, gifUrl,
landscapeGreenUrl, landscapeGreenUrl,
@ -25,17 +20,17 @@ const i18n = setupI18n('en', enMessages);
const messageLookup: Map<string, MessageSearchResultPropsType> = new Map(); const messageLookup: Map<string, MessageSearchResultPropsType> = new Map();
const CONTACT = 'contact' as 'contact'; const CONTACT = 'contact' as const;
const CONTACTS_HEADER = 'contacts-header' as 'contacts-header'; const CONTACTS_HEADER = 'contacts-header' as const;
const CONVERSATION = 'conversation' as 'conversation'; const CONVERSATION = 'conversation' as const;
const CONVERSATIONS_HEADER = 'conversations-header' as 'conversations-header'; const CONVERSATIONS_HEADER = 'conversations-header' as const;
const DIRECT = 'direct' as 'direct'; const DIRECT = 'direct' as const;
const GROUP = 'group' as 'group'; const GROUP = 'group' as const;
const MESSAGE = 'message' as 'message'; const MESSAGE = 'message' as const;
const MESSAGES_HEADER = 'messages-header' as 'messages-header'; const MESSAGES_HEADER = 'messages-header' as const;
const SENT = 'sent' as 'sent'; const SENT = 'sent' as const;
const START_NEW_CONVERSATION = 'start-new-conversation' as 'start-new-conversation'; const START_NEW_CONVERSATION = 'start-new-conversation' as const;
const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as 'sms-mms-not-supported-text'; const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as const;
messageLookup.set('1-guid-guid-guid-guid-guid', { messageLookup.set('1-guid-guid-guid-guid-guid', {
id: '1-guid-guid-guid-guid-guid', id: '1-guid-guid-guid-guid-guid',
@ -152,7 +147,7 @@ const conversations = [
name: 'Everyone 🌆', name: 'Everyone 🌆',
title: 'Everyone 🌆', title: 'Everyone 🌆',
type: GROUP, type: GROUP,
color: 'signal-blue' as 'signal-blue', color: 'signal-blue' as const,
avatarPath: landscapeGreenUrl, avatarPath: landscapeGreenUrl,
isMe: false, isMe: false,
lastUpdated: Date.now() - 5 * 60 * 1000, lastUpdated: Date.now() - 5 * 60 * 1000,
@ -171,7 +166,7 @@ const conversations = [
phoneNumber: '(202) 555-0012', phoneNumber: '(202) 555-0012',
name: 'Everyone Else 🔥', name: 'Everyone Else 🔥',
title: 'Everyone Else 🔥', title: 'Everyone Else 🔥',
color: 'pink' as 'pink', color: 'pink' as const,
type: DIRECT, type: DIRECT,
avatarPath: landscapePurpleUrl, avatarPath: landscapePurpleUrl,
isMe: false, isMe: false,
@ -194,7 +189,7 @@ const contacts = [
phoneNumber: '(202) 555-0013', phoneNumber: '(202) 555-0013',
name: 'The one Everyone', name: 'The one Everyone',
title: 'The one Everyone', title: 'The one Everyone',
color: 'blue' as 'blue', color: 'blue' as const,
type: DIRECT, type: DIRECT,
avatarPath: gifUrl, avatarPath: gifUrl,
isMe: false, isMe: false,
@ -211,7 +206,7 @@ const contacts = [
name: 'No likey everyone', name: 'No likey everyone',
title: 'No likey everyone', title: 'No likey everyone',
type: DIRECT, type: DIRECT,
color: 'red' as 'red', color: 'red' as const,
isMe: false, isMe: false,
lastUpdated: Date.now() - 11 * 60 * 1000, lastUpdated: Date.now() - 11 * 60 * 1000,
unreadCount: 0, unreadCount: 0,

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { CSSProperties } from 'react';
import { CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; import { CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import { debounce, get, isNumber } from 'lodash'; import { debounce, get, isNumber } from 'lodash';
@ -98,8 +98,8 @@ type RowRendererParamsType = {
isScrolling: boolean; isScrolling: boolean;
isVisible: boolean; isVisible: boolean;
key: string; key: string;
parent: Object; parent: Record<string, unknown>;
style: Object; style: CSSProperties;
}; };
type OnScrollParamsType = { type OnScrollParamsType = {
scrollTop: number; scrollTop: number;
@ -117,24 +117,32 @@ type OnScrollParamsType = {
export class SearchResults extends React.Component<PropsType, StateType> { export class SearchResults extends React.Component<PropsType, StateType> {
public setFocusToFirstNeeded = false; public setFocusToFirstNeeded = false;
public setFocusToLastNeeded = false; public setFocusToLastNeeded = false;
public cellSizeCache = new CellMeasurerCache({ public cellSizeCache = new CellMeasurerCache({
defaultHeight: 80, defaultHeight: 80,
fixedWidth: true, fixedWidth: true,
}); });
public listRef = React.createRef<any>();
public containerRef = React.createRef<HTMLDivElement>();
public state = {
scrollToIndex: undefined,
};
public handleStartNewConversation = () => { public listRef = React.createRef<List>();
public containerRef = React.createRef<HTMLDivElement>();
constructor(props: PropsType) {
super(props);
this.state = {
scrollToIndex: undefined,
};
}
public handleStartNewConversation = (): void => {
const { regionCode, searchTerm, startNewConversation } = this.props; const { regionCode, searchTerm, startNewConversation } = this.props;
startNewConversation(searchTerm, { regionCode }); startNewConversation(searchTerm, { regionCode });
}; };
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => { public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
const { items } = this.props; const { items } = this.props;
const commandKey = get(window, 'platform') === 'darwin' && event.metaKey; const commandKey = get(window, 'platform') === 'darwin' && event.metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey; const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey;
@ -161,12 +169,10 @@ export class SearchResults extends React.Component<PropsType, StateType> {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return;
} }
}; };
public handleFocus = () => { public handleFocus = (): void => {
const { selectedConversationId, selectedMessageId } = this.props; const { selectedConversationId, selectedMessageId } = this.props;
const { current: container } = this.containerRef; const { current: container } = this.containerRef;
@ -179,10 +185,9 @@ export class SearchResults extends React.Component<PropsType, StateType> {
// First we try to scroll to the selected message // First we try to scroll to the selected message
if (selectedMessageId && scrollingContainer) { if (selectedMessageId && scrollingContainer) {
// tslint:disable-next-line no-unnecessary-type-assertion const target: HTMLElement | null = scrollingContainer.querySelector(
const target = scrollingContainer.querySelector(
`.module-message-search-result[data-id="${selectedMessageId}"]` `.module-message-search-result[data-id="${selectedMessageId}"]`
) as any; );
if (target && target.focus) { if (target && target.focus) {
target.focus(); target.focus();
@ -197,10 +202,9 @@ export class SearchResults extends React.Component<PropsType, StateType> {
/["\\]/g, /["\\]/g,
'\\$&' '\\$&'
); );
// tslint:disable-next-line no-unnecessary-type-assertion const target: HTMLElement | null = scrollingContainer.querySelector(
const target = scrollingContainer.querySelector(
`.module-conversation-list-item[data-id="${escapedId}"]` `.module-conversation-list-item[data-id="${escapedId}"]`
) as any; );
if (target && target.focus) { if (target && target.focus) {
target.focus(); target.focus();
@ -214,14 +218,13 @@ export class SearchResults extends React.Component<PropsType, StateType> {
} }
}; };
public setFocusToFirst = () => { public setFocusToFirst = (): void => {
const { current: container } = this.containerRef; const { current: container } = this.containerRef;
if (container) { if (container) {
// tslint:disable-next-line no-unnecessary-type-assertion const noResultsItem: HTMLElement | null = container.querySelector(
const noResultsItem = container.querySelector(
'.module-search-results__no-results' '.module-search-results__no-results'
) as any; );
if (noResultsItem && noResultsItem.focus) { if (noResultsItem && noResultsItem.focus) {
noResultsItem.focus(); noResultsItem.focus();
@ -234,54 +237,51 @@ export class SearchResults extends React.Component<PropsType, StateType> {
return; return;
} }
// tslint:disable-next-line no-unnecessary-type-assertion const startItem: HTMLElement | null = scrollContainer.querySelector(
const startItem = scrollContainer.querySelector(
'.module-start-new-conversation' '.module-start-new-conversation'
) as any; );
if (startItem && startItem.focus) { if (startItem && startItem.focus) {
startItem.focus(); startItem.focus();
return; return;
} }
// tslint:disable-next-line no-unnecessary-type-assertion const conversationItem: HTMLElement | null = scrollContainer.querySelector(
const conversationItem = scrollContainer.querySelector(
'.module-conversation-list-item' '.module-conversation-list-item'
) as any; );
if (conversationItem && conversationItem.focus) { if (conversationItem && conversationItem.focus) {
conversationItem.focus(); conversationItem.focus();
return; return;
} }
// tslint:disable-next-line no-unnecessary-type-assertion const messageItem: HTMLElement | null = scrollContainer.querySelector(
const messageItem = scrollContainer.querySelector(
'.module-message-search-result' '.module-message-search-result'
) as any; );
if (messageItem && messageItem.focus) { if (messageItem && messageItem.focus) {
messageItem.focus(); messageItem.focus();
return;
} }
}; };
public getScrollContainer = () => { public getScrollContainer = (): HTMLDivElement | null => {
if (!this.listRef || !this.listRef.current) { if (!this.listRef || !this.listRef.current) {
return; return null;
} }
const list = this.listRef.current; const list = this.listRef.current;
if (!list.Grid || !list.Grid._scrollingContainer) { // We're using an internal variable (_scrollingContainer)) here,
return; // so cannot rely on the public type.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const grid: any = list.Grid;
if (!grid || !grid._scrollingContainer) {
return null;
} }
return list.Grid._scrollingContainer as HTMLDivElement; return grid._scrollingContainer as HTMLDivElement;
}; };
// tslint:disable-next-line member-ordering
public onScroll = debounce( public onScroll = debounce(
// tslint:disable-next-line cyclomatic-complexity
(data: OnScrollParamsType) => { (data: OnScrollParamsType) => {
// Ignore scroll events generated as react-virtualized recursively scrolls and // Ignore scroll events generated as react-virtualized recursively scrolls and
// re-measures to get us where we want to go. // re-measures to get us where we want to go.
@ -308,9 +308,9 @@ export class SearchResults extends React.Component<PropsType, StateType> {
return; return;
} }
const messageItems = scrollContainer.querySelectorAll( const messageItems: NodeListOf<HTMLElement> = scrollContainer.querySelectorAll(
'.module-message-search-result' '.module-message-search-result'
) as any; );
if (messageItems && messageItems.length > 0) { if (messageItems && messageItems.length > 0) {
const last = messageItems[messageItems.length - 1]; const last = messageItems[messageItems.length - 1];
@ -321,9 +321,9 @@ export class SearchResults extends React.Component<PropsType, StateType> {
} }
} }
const contactItems = scrollContainer.querySelectorAll( const contactItems: NodeListOf<HTMLElement> = scrollContainer.querySelectorAll(
'.module-conversation-list-item' '.module-conversation-list-item'
) as any; );
if (contactItems && contactItems.length > 0) { if (contactItems && contactItems.length > 0) {
const last = contactItems[contactItems.length - 1]; const last = contactItems[contactItems.length - 1];
@ -336,14 +336,12 @@ export class SearchResults extends React.Component<PropsType, StateType> {
const startItem = scrollContainer.querySelectorAll( const startItem = scrollContainer.querySelectorAll(
'.module-start-new-conversation' '.module-start-new-conversation'
) as any; ) as NodeListOf<HTMLElement>;
if (startItem && startItem.length > 0) { if (startItem && startItem.length > 0) {
const last = startItem[startItem.length - 1]; const last = startItem[startItem.length - 1];
if (last && last.focus) { if (last && last.focus) {
last.focus(); last.focus();
return;
} }
} }
} }
@ -352,7 +350,7 @@ export class SearchResults extends React.Component<PropsType, StateType> {
{ maxWait: 100 } { maxWait: 100 }
); );
public renderRowContents(row: SearchResultRowType) { public renderRowContents(row: SearchResultRowType): JSX.Element {
const { const {
searchTerm, searchTerm,
i18n, i18n,
@ -368,13 +366,15 @@ export class SearchResults extends React.Component<PropsType, StateType> {
onClick={this.handleStartNewConversation} onClick={this.handleStartNewConversation}
/> />
); );
} else if (row.type === 'sms-mms-not-supported-text') { }
if (row.type === 'sms-mms-not-supported-text') {
return ( return (
<div className="module-search-results__sms-not-supported"> <div className="module-search-results__sms-not-supported">
{i18n('notSupportedSMS')} {i18n('notSupportedSMS')}
</div> </div>
); );
} else if (row.type === 'conversations-header') { }
if (row.type === 'conversations-header') {
return ( return (
<div <div
className="module-search-results__conversations-header" className="module-search-results__conversations-header"
@ -384,7 +384,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
{i18n('conversationsHeader')} {i18n('conversationsHeader')}
</div> </div>
); );
} else if (row.type === 'conversation') { }
if (row.type === 'conversation') {
const { data } = row; const { data } = row;
return ( return (
@ -395,7 +396,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
i18n={i18n} i18n={i18n}
/> />
); );
} else if (row.type === 'contacts-header') { }
if (row.type === 'contacts-header') {
return ( return (
<div <div
className="module-search-results__contacts-header" className="module-search-results__contacts-header"
@ -405,7 +407,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
{i18n('contactsHeader')} {i18n('contactsHeader')}
</div> </div>
); );
} else if (row.type === 'contact') { }
if (row.type === 'contact') {
const { data } = row; const { data } = row;
return ( return (
@ -416,7 +419,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
i18n={i18n} i18n={i18n}
/> />
); );
} else if (row.type === 'messages-header') { }
if (row.type === 'messages-header') {
return ( return (
<div <div
className="module-search-results__messages-header" className="module-search-results__messages-header"
@ -426,21 +430,22 @@ export class SearchResults extends React.Component<PropsType, StateType> {
{i18n('messagesHeader')} {i18n('messagesHeader')}
</div> </div>
); );
} else if (row.type === 'message') { }
if (row.type === 'message') {
const { data } = row; const { data } = row;
return renderMessageSearchResult(data); return renderMessageSearchResult(data);
} else if (row.type === 'spinner') { }
if (row.type === 'spinner') {
return ( return (
<div className="module-search-results__spinner-container"> <div className="module-search-results__spinner-container">
<Spinner size="24px" svgSize="small" /> <Spinner size="24px" svgSize="small" />
</div> </div>
); );
} else {
throw new Error(
'SearchResults.renderRowContents: Encountered unknown row type'
);
} }
throw new Error(
'SearchResults.renderRowContents: Encountered unknown row type'
);
} }
public renderRow = ({ public renderRow = ({
@ -469,7 +474,7 @@ export class SearchResults extends React.Component<PropsType, StateType> {
); );
}; };
public componentDidUpdate(prevProps: PropsType) { public componentDidUpdate(prevProps: PropsType): void {
const { const {
items, items,
searchTerm, searchTerm,
@ -493,9 +498,9 @@ export class SearchResults extends React.Component<PropsType, StateType> {
} }
} }
public getList = () => { public getList = (): List | null => {
if (!this.listRef) { if (!this.listRef) {
return; return null;
} }
const { current } = this.listRef; const { current } = this.listRef;
@ -503,7 +508,7 @@ export class SearchResults extends React.Component<PropsType, StateType> {
return current; return current;
}; };
public recomputeRowHeights = (row?: number) => { public recomputeRowHeights = (row?: number): void => {
const list = this.getList(); const list = this.getList();
if (!list) { if (!list) {
return; return;
@ -512,18 +517,18 @@ export class SearchResults extends React.Component<PropsType, StateType> {
list.recomputeRowHeights(row); list.recomputeRowHeights(row);
}; };
public resizeAll = () => { public resizeAll = (): void => {
this.cellSizeCache.clearAll(); this.cellSizeCache.clearAll();
this.recomputeRowHeights(0); this.recomputeRowHeights(0);
}; };
public getRowCount() { public getRowCount(): number {
const { items } = this.props; const { items } = this.props;
return items ? items.length : 0; return items ? items.length : 0;
} }
public render() { public render(): JSX.Element {
const { const {
height, height,
i18n, i18n,
@ -574,7 +579,7 @@ export class SearchResults extends React.Component<PropsType, StateType> {
<div <div
className="module-search-results" className="module-search-results"
aria-live="polite" aria-live="polite"
role="group" role="presentation"
tabIndex={-1} tabIndex={-1}
ref={this.containerRef} ref={this.containerRef}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
@ -592,6 +597,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
rowRenderer={this.renderRow} rowRenderer={this.renderRow}
scrollToIndex={scrollToIndex} scrollToIndex={scrollToIndex}
tabIndex={-1} tabIndex={-1}
// TODO: DESKTOP-687
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onScroll={this.onScroll as any} onScroll={this.onScroll as any}
width={width} width={width}
/> />

View file

@ -3,12 +3,8 @@ import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean, select } from '@storybook/addon-knobs'; import { boolean, select } from '@storybook/addon-knobs';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { Props, ShortcutGuide } from './ShortcutGuide'; import { Props, ShortcutGuide } from './ShortcutGuide';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -196,7 +196,7 @@ const CALLING_SHORTCUTS: Array<ShortcutType> = [
}, },
]; ];
export const ShortcutGuide = (props: Props) => { export const ShortcutGuide = (props: Props): JSX.Element => {
const focusRef = React.useRef<HTMLDivElement>(null); const focusRef = React.useRef<HTMLDivElement>(null);
const { i18n, close, hasInstalledStickers, platform } = props; const { i18n, close, hasInstalledStickers, platform } = props;
const isMacOS = platform === 'darwin'; const isMacOS = platform === 'darwin';
@ -211,9 +211,11 @@ export const ShortcutGuide = (props: Props) => {
{i18n('Keyboard--header')} {i18n('Keyboard--header')}
</div> </div>
<button <button
aria-label={i18n('close-popup')}
className="module-shortcut-guide__header-close" className="module-shortcut-guide__header-close"
onClick={close} onClick={close}
title={i18n('close-popup')} title={i18n('close-popup')}
type="button"
/> />
</div> </div>
<div <div
@ -282,17 +284,17 @@ function renderShortcut(
i18n: LocalizerType i18n: LocalizerType
) { ) {
return ( return (
<div key={index} className="module-shortcut-guide__shortcut" tabIndex={0}> <div key={index} className="module-shortcut-guide__shortcut">
<div className="module-shortcut-guide__shortcut__description"> <div className="module-shortcut-guide__shortcut__description">
{i18n(shortcut.description)} {i18n(shortcut.description)}
</div> </div>
<div className="module-shortcut-guide__shortcut__key-container"> <div className="module-shortcut-guide__shortcut__key-container">
{shortcut.keys.map((keys, outerIndex) => ( {shortcut.keys.map(keys => (
<div <div
key={outerIndex} key={`${shortcut.description}--${keys.map(k => k).join('-')}`}
className="module-shortcut-guide__shortcut__key-inner-container" className="module-shortcut-guide__shortcut__key-inner-container"
> >
{keys.map((key, mapIndex) => { {keys.map(key => {
let label: string = key; let label: string = key;
let isSquare = true; let isSquare = true;
@ -334,7 +336,7 @@ function renderShortcut(
return ( return (
<span <span
key={mapIndex} key={`shortcut__key--${key}`}
className={classNames( className={classNames(
'module-shortcut-guide__shortcut__key', 'module-shortcut-guide__shortcut__key',
isSquare isSquare

View file

@ -10,36 +10,33 @@ export type PropsType = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
}; };
export const ShortcutGuideModal = React.memo( export const ShortcutGuideModal = React.memo((props: PropsType) => {
// tslint:disable-next-line max-func-body-length const { i18n, close, hasInstalledStickers, platform } = props;
(props: PropsType) => { const [root, setRoot] = React.useState<HTMLElement | null>(null);
const { i18n, close, hasInstalledStickers, platform } = props;
const [root, setRoot] = React.useState<HTMLElement | null>(null);
React.useEffect(() => { React.useEffect(() => {
const div = document.createElement('div'); const div = document.createElement('div');
document.body.appendChild(div); document.body.appendChild(div);
setRoot(div); setRoot(div);
return () => { return () => {
document.body.removeChild(div); document.body.removeChild(div);
}; };
}, []); }, []);
return root return root
? createPortal( ? createPortal(
<div className="module-shortcut-guide-modal"> <div className="module-shortcut-guide-modal">
<div className="module-shortcut-guide-container"> <div className="module-shortcut-guide-container">
<ShortcutGuide <ShortcutGuide
hasInstalledStickers={hasInstalledStickers} hasInstalledStickers={hasInstalledStickers}
platform={platform} platform={platform}
close={close} close={close}
i18n={i18n} i18n={i18n}
/> />
</div> </div>
</div>, </div>,
root root
) )
: null; : null;
} });
);

View file

@ -1,8 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { Props, Spinner, SpinnerDirections, SpinnerSvgSizes } from './Spinner';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { select, text } from '@storybook/addon-knobs'; import { select, text } from '@storybook/addon-knobs';
import { Props, Spinner, SpinnerDirections, SpinnerSvgSizes } from './Spinner';
const story = storiesOf('Components/Spinner', module); const story = storiesOf('Components/Spinner', module);

View file

@ -17,42 +17,34 @@ export interface Props {
direction?: SpinnerDirection; direction?: SpinnerDirection;
} }
export class Spinner extends React.Component<Props> { export const Spinner = ({ size, svgSize, direction }: Props): JSX.Element => (
public render() { <div
const { size, svgSize, direction } = this.props; className={classNames(
'module-spinner__container',
return ( `module-spinner__container--${svgSize}`,
<div direction ? `module-spinner__container--${direction}` : null,
className={classNames( direction ? `module-spinner__container--${svgSize}-${direction}` : null
'module-spinner__container', )}
`module-spinner__container--${svgSize}`, style={{
direction ? `module-spinner__container--${direction}` : null, height: size,
direction width: size,
? `module-spinner__container--${svgSize}-${direction}` }}
: null >
)} <div
style={{ className={classNames(
height: size, 'module-spinner__circle',
width: size, `module-spinner__circle--${svgSize}`,
}} direction ? `module-spinner__circle--${direction}` : null,
> direction ? `module-spinner__circle--${svgSize}-${direction}` : null
<div )}
className={classNames( />
'module-spinner__circle', <div
`module-spinner__circle--${svgSize}`, className={classNames(
direction ? `module-spinner__circle--${direction}` : null, 'module-spinner__arc',
direction ? `module-spinner__circle--${svgSize}-${direction}` : null `module-spinner__arc--${svgSize}`,
)} direction ? `module-spinner__arc--${direction}` : null,
/> direction ? `module-spinner__arc--${svgSize}-${direction}` : null
<div )}
className={classNames( />
'module-spinner__arc', </div>
`module-spinner__arc--${svgSize}`, );
direction ? `module-spinner__arc--${direction}` : null,
direction ? `module-spinner__arc--${svgSize}-${direction}` : null
)}
/>
</div>
);
}
}

View file

@ -1,15 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { Props, StartNewConversation } from './StartNewConversation';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
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 { Props, StartNewConversation } from './StartNewConversation';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -11,11 +11,15 @@ export interface Props {
} }
export class StartNewConversation extends React.PureComponent<Props> { export class StartNewConversation extends React.PureComponent<Props> {
public render() { public render(): JSX.Element {
const { phoneNumber, i18n, onClick } = this.props; const { phoneNumber, i18n, onClick } = this.props;
return ( return (
<button className="module-start-new-conversation" onClick={onClick}> <button
type="button"
className="module-start-new-conversation"
onClick={onClick}
>
<Avatar <Avatar
color="grey" color="grey"
conversationType="direct" conversationType="direct"

View file

@ -1,14 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { UpdateDialog } from './UpdateDialog';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean, select } from '@storybook/addon-knobs'; import { boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { UpdateDialog } from './UpdateDialog';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -81,7 +81,9 @@ export const UpdateDialog = ({
</span> </span>
</div> </div>
<div className="module-left-pane-dialog__actions"> <div className="module-left-pane-dialog__actions">
<button onClick={dismissDialog}>{i18n('ok')}</button> <button type="button" onClick={dismissDialog}>
{i18n('ok')}
</button>
</div> </div>
</div> </div>
); );
@ -96,13 +98,14 @@ export const UpdateDialog = ({
<div className="module-left-pane-dialog__actions"> <div className="module-left-pane-dialog__actions">
{!didSnooze && ( {!didSnooze && (
<button <button
type="button"
className="module-left-pane-dialog__button--no-border" className="module-left-pane-dialog__button--no-border"
onClick={snoozeUpdate} onClick={snoozeUpdate}
> >
{i18n('autoUpdateLaterButtonLabel')} {i18n('autoUpdateLaterButtonLabel')}
</button> </button>
)} )}
<button onClick={startUpdate}> <button type="button" onClick={startUpdate}>
{i18n('autoUpdateRestartButtonLabel')} {i18n('autoUpdateRestartButtonLabel')}
</button> </button>
</div> </div>

View file

@ -1,6 +1,4 @@
// A separate file so this doesn't get picked up by StyleGuidist over real components import { MutableRefObject, Ref } from 'react';
import { Ref } from 'react';
import { isFunction } from 'lodash'; import { isFunction } from 'lodash';
import memoizee from 'memoizee'; import memoizee from 'memoizee';
@ -8,6 +6,8 @@ export function cleanId(id: string): string {
return id.replace(/[^\u0020-\u007e\u00a0-\u00ff]/g, '_'); return id.replace(/[^\u0020-\u007e\u00a0-\u00ff]/g, '_');
} }
// Memoizee makes this difficult.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const createRefMerger = () => export const createRefMerger = () =>
memoizee( memoizee(
<T>(...refs: Array<Ref<T>>) => { <T>(...refs: Array<Ref<T>>) => {
@ -16,8 +16,9 @@ export const createRefMerger = () =>
if (isFunction(r)) { if (isFunction(r)) {
r(t); r(t);
} else if (r) { } else if (r) {
// @ts-ignore: React's typings for ref objects is annoying // Using a MutableRefObject as intended
r.current = t; // eslint-disable-next-line no-param-reassign
(r as MutableRefObject<T>).current = t;
} }
}); });
}; };

View file

@ -108,8 +108,7 @@ export const preloadImages = async (): Promise<void> => {
setTimeout(reject, 5000); setTimeout(reject, 5000);
}); });
// eslint-disable-next-line no-console window.log.info('Preloading emoji images');
console.log('Preloading emoji images');
const start = Date.now(); const start = Date.now();
data.forEach(emoji => { data.forEach(emoji => {
@ -127,8 +126,7 @@ export const preloadImages = async (): Promise<void> => {
await imageQueue.onEmpty(); await imageQueue.onEmpty();
const end = Date.now(); const end = Date.now();
// eslint-disable-next-line no-console window.log.info(`Done preloading emoji images in ${end - start}ms`);
console.log(`Done preloading emoji images in ${end - start}ms`);
}; };
const dataByShortName = keyBy(data, 'short_name'); const dataByShortName = keyBy(data, 'short_name');

View file

@ -12829,7 +12829,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/CallScreen.js", "path": "ts/components/CallScreen.js",
"line": " this.localVideoRef = react_1.default.createRef();", "line": " this.localVideoRef = react_1.default.createRef();",
"lineNumber": 97, "lineNumber": 98,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:22:06.472Z", "updated": "2020-05-28T17:22:06.472Z",
"reasonDetail": "Used to render local preview video" "reasonDetail": "Used to render local preview video"
@ -12847,7 +12847,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/CallScreen.tsx", "path": "ts/components/CallScreen.tsx",
"line": " this.localVideoRef = React.createRef();", "line": " this.localVideoRef = React.createRef();",
"lineNumber": 74, "lineNumber": 79,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-06-02T21:51:34.813Z", "updated": "2020-06-02T21:51:34.813Z",
"reasonDetail": "Used to render local preview video" "reasonDetail": "Used to render local preview video"
@ -12874,7 +12874,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/CaptionEditor.tsx", "path": "ts/components/CaptionEditor.tsx",
"line": " this.inputRef = React.createRef();", "line": " this.inputRef = React.createRef();",
"lineNumber": 46, "lineNumber": 50,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-03-09T00:08:44.242Z", "updated": "2019-03-09T00:08:44.242Z",
"reasonDetail": "Used only to set focus" "reasonDetail": "Used only to set focus"
@ -12883,7 +12883,7 @@
"rule": "DOM-innerHTML", "rule": "DOM-innerHTML",
"path": "ts/components/CompositionArea.js", "path": "ts/components/CompositionArea.js",
"line": " el.innerHTML = '';", "line": " el.innerHTML = '';",
"lineNumber": 23, "lineNumber": 24,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-20T20:10:43.540Z", "updated": "2020-05-20T20:10:43.540Z",
"reasonDetail": "Our code, no user input, only clearing out the dom" "reasonDetail": "Our code, no user input, only clearing out the dom"
@ -12892,7 +12892,7 @@
"rule": "DOM-innerHTML", "rule": "DOM-innerHTML",
"path": "ts/components/CompositionArea.tsx", "path": "ts/components/CompositionArea.tsx",
"line": " el.innerHTML = '';", "line": " el.innerHTML = '';",
"lineNumber": 80, "lineNumber": 81,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-06-03T19:23:21.195Z", "updated": "2020-06-03T19:23:21.195Z",
"reasonDetail": "Our code, no user input, only clearing out the dom" "reasonDetail": "Our code, no user input, only clearing out the dom"
@ -12901,7 +12901,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "ts/components/Intl.js", "path": "ts/components/Intl.js",
"line": " const FIND_REPLACEMENTS = /\\$([^$]+)\\$/g;", "line": " const FIND_REPLACEMENTS = /\\$([^$]+)\\$/g;",
"lineNumber": 35, "lineNumber": 33,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-07-21T18:34:59.251Z" "updated": "2020-07-21T18:34:59.251Z"
}, },
@ -12935,7 +12935,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/Lightbox.js", "path": "ts/components/Lightbox.js",
"line": " this.containerRef = react_1.default.createRef();", "line": " this.containerRef = react_1.default.createRef();",
"lineNumber": 141, "lineNumber": 148,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-11-06T19:56:38.557Z", "updated": "2019-11-06T19:56:38.557Z",
"reasonDetail": "Used to double-check outside clicks" "reasonDetail": "Used to double-check outside clicks"
@ -12953,7 +12953,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/Lightbox.js", "path": "ts/components/Lightbox.js",
"line": " this.focusRef = react_1.default.createRef();", "line": " this.focusRef = react_1.default.createRef();",
"lineNumber": 143, "lineNumber": 150,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-11-06T19:56:38.557Z", "updated": "2019-11-06T19:56:38.557Z",
"reasonDetail": "Used to manage focus" "reasonDetail": "Used to manage focus"
@ -12962,7 +12962,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/MainHeader.js", "path": "ts/components/MainHeader.js",
"line": " this.inputRef = react_1.default.createRef();", "line": " this.inputRef = react_1.default.createRef();",
"lineNumber": 146, "lineNumber": 144,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Used only to set focus" "reasonDetail": "Used only to set focus"

View file

@ -178,9 +178,10 @@
"linterOptions": { "linterOptions": {
"exclude": [ "exclude": [
"ts/*.ts", "ts/*.ts",
"ts/components/emoji/**",
"ts/backbone/**", "ts/backbone/**",
"ts/build/**", "ts/build/**",
"ts/components/*.ts[x]",
"ts/components/emoji/**",
"ts/notifications/**", "ts/notifications/**",
"ts/protobuf/**", "ts/protobuf/**",
"ts/scripts/**", "ts/scripts/**",