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
# JIRA: DESKTOP-304
ts/components/*.ts
ts/components/*.tsx
sticker-creator/**/*.ts
sticker-creator/**/*.tsx
ts/*.ts
ts/components/conversation/**
ts/components/stickers/**
ts/shims/**
@ -44,5 +45,3 @@ ts/textsecure/**
ts/types/**
ts/updater/**
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',
// Prefer functional components with default params
'react/require-default-props': 'off',
};
module.exports = {
@ -101,7 +118,6 @@ module.exports = {
rules: {
...rules,
'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
},
},
],

View File

@ -671,6 +671,10 @@
"message": "Search",
"description": "Placeholder text in the search input"
},
"clearSearch": {
"message": "Clear Search",
"description": "Aria label for clear search button"
},
"searchIn": {
"message": "Search in $conversationName$",
"description": "Shown in the search box before text is entered when searching in a specific conversation",
@ -3568,5 +3572,25 @@
"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 { Avatar, Props } from './Avatar';
import { storiesOf } from '@storybook/react';
import { boolean, select, text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
// @ts-ignore
import { Avatar, Props } from './Avatar';
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { Colors, ColorType } from '../types/Colors';

View File

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

View File

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

View File

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

View File

@ -1,16 +1,13 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { CallManager } from './CallManager';
import { CallState } from '../types/Calling';
import { 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 { action } from '@storybook/addon-actions';
const i18n = setupI18n('en', enMessages);
const callDetails = {

View File

@ -1,17 +1,14 @@
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 { boolean, select } from '@storybook/addon-knobs';
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 callDetails = {

View File

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

View File

@ -1,14 +1,11 @@
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 { 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 audioDevice = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,13 @@
import * as React from 'react';
import 'draft-js/dist/Draft.css';
import { boolean, select } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
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';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { boolean, select } from '@storybook/addon-knobs';
const i18n = setupI18n('en', enMessages);

View File

@ -63,7 +63,7 @@ function getTrimmedMatchAtIndex(str: string, index: number, pattern: RegExp) {
// Reset regex state
pattern.exec('');
// tslint:disable-next-line no-conditional-assignment
// eslint-disable-next-line no-cond-assign
while ((match = pattern.exec(str))) {
const matchStr = match.toString();
const start = match.index + (matchStr.length - matchStr.trimLeft().length);
@ -155,7 +155,7 @@ const compositeDecorator = new CompositeDecorator([
const text = block.getText();
let match;
let index;
// tslint:disable-next-line no-conditional-assignment
// eslint-disable-next-line no-cond-assign
while ((match = pat.exec(text))) {
index = match.index;
cb(index, index + match[0].length);
@ -174,7 +174,7 @@ const compositeDecorator = new CompositeDecorator([
<Emoji
shortName={contentState.getEntity(entityKey).getData().shortName}
skinTone={contentState.getEntity(entityKey).getData().skinTone}
inline={true}
inline
size={20}
>
{children}
@ -204,7 +204,6 @@ const getInitialEditorState = (startingText?: string) => {
return EditorState.forceSelection(state, selectionAtEnd);
};
// tslint:disable-next-line max-func-body-length
export const CompositionInput = ({
i18n,
disabled,
@ -221,7 +220,7 @@ export const CompositionInput = ({
startingText,
getQuotedMessage,
clearQuotedMessage,
}: Props) => {
}: Props): JSX.Element => {
const [editorRenderState, setEditorRenderState] = React.useState(
getInitialEditorState(startingText)
);
@ -299,119 +298,18 @@ export const CompositionInput = ({
setSearchText('');
}, [setEmojiResults, setEmojiResultsIndex, setSearchText]);
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
const getWordAtCaret = React.useCallback((state = editorStateRef.current) => {
const selection = state.getSelection();
const index = selection.getAnchorOffset();
return getWordAtIndex(
state
.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;
} 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]
);
.getText(),
index
);
}, []);
const selectEmojiResult = React.useCallback(
(dir: 'next' | 'prev', e?: React.KeyboardEvent) => {
@ -445,93 +343,17 @@ export const CompositionInput = ({
}
}
},
[emojiResultsIndex, emojiResults]
[emojiResults]
);
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();
}
},
[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 submit = React.useCallback(() => {
const { current: state } = editorStateRef;
const trimmedText = state
.getCurrentContent()
.getPlainText()
.trim();
onSubmit(trimmedText);
}, [editorStateRef, onSubmit]);
const handleEditorCommand = React.useCallback(
(
@ -604,9 +426,12 @@ export const CompositionInput = ({
return 'not-handled';
},
// Missing `onPickEmoji`, which is a prop, so not clearly memoized
// eslint-disable-next-line react-hooks/exhaustive-deps
[
emojiResults,
emojiResultsIndex,
getWordAtCaret,
resetEmojiResults,
selectEmojiResult,
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(
(e: React.KeyboardEvent) => {
if (e.shiftKey || emojiResults.length === 0) {
@ -624,11 +627,10 @@ export const CompositionInput = ({
e.preventDefault();
handleEditorCommand('enter-emoji', editorStateRef.current);
},
[emojiResults, editorStateRef, handleEditorCommand, resetEmojiResults]
[emojiResults, editorStateRef, handleEditorCommand]
);
const editorKeybindingFn = React.useCallback(
// tslint:disable-next-line cyclomatic-complexity
(e: React.KeyboardEvent): CompositionInputEditorCommand | null => {
const commandKey = get(window, 'platform') === 'darwin' && e.metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && e.ctrlKey;
@ -718,7 +720,8 @@ export const CompositionInput = ({
// Manage 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}
// https://github.com/facebook/react/issues/6410
React.useLayoutEffect(() => {
@ -744,6 +747,8 @@ export const CompositionInput = ({
}, [editorStateRef, rootElRef, setAndTrackEditorState]);
if (inputApi) {
// Using a React.MutableRefObject, so we need to reassign this prop.
// eslint-disable-next-line no-param-reassign
inputApi.current = {
reset: resetEditorState,
submit,
@ -756,7 +761,7 @@ export const CompositionInput = ({
<Manager>
<Reference>
{({ ref: popperRef }) => (
<Measure bounds={true} onResize={handleEditorSizeChange}>
<Measure bounds onResize={handleEditorSizeChange}>
{({ measureRef }) => (
<div
className="module-composition-input__input"
@ -783,8 +788,8 @@ export const CompositionInput = ({
handleBeforeInput={handleBeforeInput}
handlePastedText={handlePastedText}
keyBindingFn={editorKeybindingFn}
spellCheck={true}
stripPastedStyles={true}
spellCheck
stripPastedStyles
readOnly={disabled}
onFocus={onFocus}
onBlur={onBlur}
@ -807,11 +812,13 @@ export const CompositionInput = ({
width: editorWidth,
}}
role="listbox"
aria-expanded={true}
aria-expanded
aria-activedescendant={`emoji-result--${emojiResults[emojiResultsIndex].short_name}`}
tabIndex={0}
>
{emojiResults.map((emoji, index) => (
<button
type="button"
key={emoji.short_name}
id={`emoji-result--${emoji.short_name}`}
role="option button"

View File

@ -1,15 +1,12 @@
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 { action } from '@storybook/addon-actions';
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);
storiesOf('Components/ConfirmationDialog', module).add(

View File

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

View File

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

View File

@ -4,12 +4,8 @@ import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { gifUrl } from '../storybook/Fixtures';
// @ts-ignore
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';
const i18n = setupI18n('en', enMessages);

View File

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

View File

@ -1,22 +1,17 @@
import * as React from 'react';
import 'draft-js/dist/Draft.css';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { boolean, date, select, text } from '@storybook/addon-knobs';
import {
ConversationListItem,
MessageStatuses,
Props,
} from './ConversationListItem';
// tslint:disable-next-line
import 'draft-js/dist/Draft.css';
// @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 { boolean, date, select, text } from '@storybook/addon-knobs';
const i18n = setupI18n('en', enMessages);
@ -191,7 +186,6 @@ Line 4, well.`,
return messages.map(message => {
const props = createProps({
name,
lastMessage: {
text: message,
status: 'read',
@ -212,7 +206,6 @@ story.add('Various Times', () => {
return times.map(([lastUpdated, messageText]) => {
const props = createProps({
name,
lastUpdated,
lastMessage: {
text: messageText,
@ -227,12 +220,14 @@ story.add('Various Times', () => {
story.add('Missing Date', () => {
const props = createProps();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return <ConversationListItem {...props} lastUpdated={undefined as any} />;
});
story.add('Missing Message', () => {
const props = createProps();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return <ConversationListItem {...props} lastMessage={undefined as any} />;
});
@ -242,6 +237,7 @@ story.add('Missing Text', () => {
return (
<ConversationListItem
{...props}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 { isNumber } from 'lodash';
@ -43,7 +43,7 @@ export type PropsData = {
draftPreview?: string;
shouldShowDraft?: boolean;
typingContact?: Object;
typingContact?: unknown;
lastMessage?: {
status: MessageStatusType;
text: string;
@ -53,14 +53,14 @@ export type PropsData = {
type PropsHousekeeping = {
i18n: LocalizerType;
style?: Object;
style?: CSSProperties;
onClick?: (id: string) => void;
};
export type Props = PropsData & PropsHousekeeping;
export class ConversationListItem extends React.PureComponent<Props> {
public renderAvatar() {
public renderAvatar(): JSX.Element {
const {
avatarPath,
color,
@ -92,7 +92,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
);
}
public renderUnread() {
public renderUnread(): JSX.Element | null {
const { unreadCount } = this.props;
if (isNumber(unreadCount) && unreadCount > 0) {
@ -106,7 +106,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
return null;
}
public renderHeader() {
public renderHeader(): JSX.Element {
const {
unreadCount,
i18n,
@ -162,7 +162,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
);
}
public renderMessage() {
public renderMessage(): JSX.Element | null {
const {
draftPreview,
i18n,
@ -185,6 +185,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
// Note: instead of re-using showingDraft here we explode it because
// typescript can't tell that draftPreview is truthy otherwise
// Avoiding touching logic to fix linting
/* eslint-disable no-nested-ternary */
const text =
shouldShowDraft && draftPreview
? draftPreview
@ -225,8 +227,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
) : null}
<MessageBody
text={text.split('\n')[0]}
disableJumbomoji={true}
disableLinks={true}
disableJumbomoji
disableLinks
i18n={i18n}
/>
</>
@ -243,13 +245,15 @@ export class ConversationListItem extends React.PureComponent<Props> {
</div>
);
}
/* eslint-enable no-nested-ternary */
public render() {
public render(): JSX.Element {
const { unreadCount, onClick, id, isSelected, style } = this.props;
const withUnread = isNumber(unreadCount) && unreadCount > 0;
return (
<button
type="button"
onClick={() => {
if (onClick) {
onClick(id);

View File

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

View File

@ -1,14 +1,11 @@
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 { 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);
storiesOf('Components/ExpiredBuildDialog', module).add(

View File

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

View File

@ -2,11 +2,8 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react';
// @ts-ignore
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';
const i18n = setupI18n('en', enMessages);

View File

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

View File

@ -1,16 +1,13 @@
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 { boolean, select, text } from '@storybook/addon-knobs';
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 defaultProps = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,14 @@
import * as React from '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 { 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';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/LightboxGallery', module);

View File

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

View File

@ -3,11 +3,8 @@ import { storiesOf } from '@storybook/react';
import { text, withKnobs } from '@storybook/addon-knobs';
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 { MainHeader, PropsType } from './MainHeader';
const i18n = setupI18n('en', enMessages);
@ -19,6 +16,8 @@ const requiredText = (name: string, value: string | undefined) =>
const optionalText = (name: string, value: string | 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 }));
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;
// 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;
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') {
this.hideAvatarPopup();
}
};
public showAvatarPopup = () => {
public showAvatarPopup = (): void => {
const popperRoot = document.createElement('div');
document.body.appendChild(popperRoot);
@ -122,7 +122,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
document.addEventListener('keydown', this.handleOutsideKeyDown);
};
public hideAvatarPopup = () => {
public hideAvatarPopup = (): void => {
const { popperRoot } = this.state;
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;
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) => {
public search = debounce((searchTerm: string): void => {
const {
i18n,
ourConversationId,
@ -179,7 +178,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
}
}, 200);
public updateSearch = (event: React.FormEvent<HTMLInputElement>) => {
public updateSearch = (event: React.FormEvent<HTMLInputElement>): void => {
const {
updateSearchTerm,
clearConversationSearch,
@ -209,21 +208,23 @@ export class MainHeader extends React.Component<PropsType, StateType> {
this.search(searchTerm);
};
public clearSearch = () => {
public clearSearch = (): void => {
const { clearSearch } = this.props;
clearSearch();
this.setFocus();
};
public clearConversationSearch = () => {
public clearConversationSearch = (): void => {
const { clearConversationSearch } = this.props;
clearConversationSearch();
this.setFocus();
};
public handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
public handleKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>
): void => {
const {
clearConversationSearch,
clearSearch,
@ -258,7 +259,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
event.stopPropagation();
};
public handleXButton = () => {
public handleXButton = (): void => {
const {
searchConversationId,
clearConversationSearch,
@ -274,22 +275,19 @@ export class MainHeader extends React.Component<PropsType, StateType> {
this.setFocus();
};
public setFocus = () => {
public setFocus = (): void => {
if (this.inputRef.current) {
// @ts-ignore
this.inputRef.current.focus();
}
};
public setSelected = () => {
public setSelected = (): void => {
if (this.inputRef.current) {
// @ts-ignore
this.inputRef.current.select();
}
};
// tslint:disable-next-line:max-func-body-length
public render() {
public render(): JSX.Element {
const {
avatarPath,
color,
@ -366,6 +364,8 @@ export class MainHeader extends React.Component<PropsType, StateType> {
className="module-main-header__search__in-conversation-pill"
onClick={this.clearSearch}
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" />
@ -377,6 +377,8 @@ export class MainHeader extends React.Component<PropsType, StateType> {
className="module-main-header__search__icon"
onClick={this.setFocus}
tabIndex={-1}
type="button"
aria-label={i18n('search')}
/>
)}
<input
@ -402,6 +404,8 @@ export class MainHeader extends React.Component<PropsType, StateType> {
tabIndex={-1}
className="module-main-header__search__cancel-icon"
onClick={this.handleXButton}
type="button"
aria-label={i18n('cancel')}
/>
) : null}
</div>

View File

@ -2,17 +2,16 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { text, withKnobs } from '@storybook/addon-knobs';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { MessageBodyHighlight, Props } from './MessageBodyHighlight';
const i18n = setupI18n('en', enMessages);
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 }));
const createProps = (overrideProps: Partial<Props> = {}): Props => ({

View File

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

View File

@ -1,18 +1,17 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
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 { 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 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 }));
const someone = {
@ -41,8 +40,8 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
'snippet',
overrideProps.snippet || "What's <<left>>going<<right>> on?"
),
from: overrideProps.from as any,
to: overrideProps.to as any,
from: overrideProps.from as PropsType['from'],
to: overrideProps.to as PropsType['to'],
isSelected: boolean('isSelected', overrideProps.isSelected || false),
openConversationInternal: action('openConversationInternal'),
isSearchingInConversation: boolean(

View File

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

View File

@ -1,15 +1,12 @@
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 { boolean, select } from '@storybook/addon-knobs';
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 defaultProps = {

View File

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

View File

@ -1,15 +1,12 @@
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 { boolean } from '@storybook/addon-knobs';
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 defaultProps = {

View File

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

View File

@ -1,15 +1,12 @@
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 { 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 contactWithAllData = {

View File

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

View File

@ -1,16 +1,13 @@
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 { boolean, text } from '@storybook/addon-knobs';
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 contactWithAllData = {

View File

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

View File

@ -1,19 +1,14 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { SearchResults } from './SearchResults';
import {
MessageSearchResult,
PropsDataType as MessageSearchResultPropsType,
} from './MessageSearchResult';
// @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 { boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import {
gifUrl,
landscapeGreenUrl,
@ -25,17 +20,17 @@ const i18n = setupI18n('en', enMessages);
const messageLookup: Map<string, MessageSearchResultPropsType> = new Map();
const CONTACT = 'contact' as 'contact';
const CONTACTS_HEADER = 'contacts-header' as 'contacts-header';
const CONVERSATION = 'conversation' as 'conversation';
const CONVERSATIONS_HEADER = 'conversations-header' as 'conversations-header';
const DIRECT = 'direct' as 'direct';
const GROUP = 'group' as 'group';
const MESSAGE = 'message' as 'message';
const MESSAGES_HEADER = 'messages-header' as 'messages-header';
const SENT = 'sent' as 'sent';
const START_NEW_CONVERSATION = 'start-new-conversation' as 'start-new-conversation';
const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as 'sms-mms-not-supported-text';
const CONTACT = 'contact' as const;
const CONTACTS_HEADER = 'contacts-header' as const;
const CONVERSATION = 'conversation' as const;
const CONVERSATIONS_HEADER = 'conversations-header' as const;
const DIRECT = 'direct' as const;
const GROUP = 'group' as const;
const MESSAGE = 'message' as const;
const MESSAGES_HEADER = 'messages-header' as const;
const SENT = 'sent' as const;
const START_NEW_CONVERSATION = 'start-new-conversation' as const;
const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as const;
messageLookup.set('1-guid-guid-guid-guid-guid', {
id: '1-guid-guid-guid-guid-guid',
@ -152,7 +147,7 @@ const conversations = [
name: 'Everyone 🌆',
title: 'Everyone 🌆',
type: GROUP,
color: 'signal-blue' as 'signal-blue',
color: 'signal-blue' as const,
avatarPath: landscapeGreenUrl,
isMe: false,
lastUpdated: Date.now() - 5 * 60 * 1000,
@ -171,7 +166,7 @@ const conversations = [
phoneNumber: '(202) 555-0012',
name: 'Everyone Else 🔥',
title: 'Everyone Else 🔥',
color: 'pink' as 'pink',
color: 'pink' as const,
type: DIRECT,
avatarPath: landscapePurpleUrl,
isMe: false,
@ -194,7 +189,7 @@ const contacts = [
phoneNumber: '(202) 555-0013',
name: 'The one Everyone',
title: 'The one Everyone',
color: 'blue' as 'blue',
color: 'blue' as const,
type: DIRECT,
avatarPath: gifUrl,
isMe: false,
@ -211,7 +206,7 @@ const contacts = [
name: 'No likey everyone',
title: 'No likey everyone',
type: DIRECT,
color: 'red' as 'red',
color: 'red' as const,
isMe: false,
lastUpdated: Date.now() - 11 * 60 * 1000,
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 { debounce, get, isNumber } from 'lodash';
@ -98,8 +98,8 @@ type RowRendererParamsType = {
isScrolling: boolean;
isVisible: boolean;
key: string;
parent: Object;
style: Object;
parent: Record<string, unknown>;
style: CSSProperties;
};
type OnScrollParamsType = {
scrollTop: number;
@ -117,24 +117,32 @@ type OnScrollParamsType = {
export class SearchResults extends React.Component<PropsType, StateType> {
public setFocusToFirstNeeded = false;
public setFocusToLastNeeded = false;
public cellSizeCache = new CellMeasurerCache({
defaultHeight: 80,
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;
startNewConversation(searchTerm, { regionCode });
};
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
const { items } = this.props;
const commandKey = get(window, 'platform') === 'darwin' && event.metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey;
@ -161,12 +169,10 @@ export class SearchResults extends React.Component<PropsType, StateType> {
event.preventDefault();
event.stopPropagation();
return;
}
};
public handleFocus = () => {
public handleFocus = (): void => {
const { selectedConversationId, selectedMessageId } = this.props;
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
if (selectedMessageId && scrollingContainer) {
// tslint:disable-next-line no-unnecessary-type-assertion
const target = scrollingContainer.querySelector(
const target: HTMLElement | null = scrollingContainer.querySelector(
`.module-message-search-result[data-id="${selectedMessageId}"]`
) as any;
);
if (target && target.focus) {
target.focus();
@ -197,10 +202,9 @@ export class SearchResults extends React.Component<PropsType, StateType> {
/["\\]/g,
'\\$&'
);
// tslint:disable-next-line no-unnecessary-type-assertion
const target = scrollingContainer.querySelector(
const target: HTMLElement | null = scrollingContainer.querySelector(
`.module-conversation-list-item[data-id="${escapedId}"]`
) as any;
);
if (target && 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;
if (container) {
// tslint:disable-next-line no-unnecessary-type-assertion
const noResultsItem = container.querySelector(
const noResultsItem: HTMLElement | null = container.querySelector(
'.module-search-results__no-results'
) as any;
);
if (noResultsItem && noResultsItem.focus) {
noResultsItem.focus();
@ -234,54 +237,51 @@ export class SearchResults extends React.Component<PropsType, StateType> {
return;
}
// tslint:disable-next-line no-unnecessary-type-assertion
const startItem = scrollContainer.querySelector(
const startItem: HTMLElement | null = scrollContainer.querySelector(
'.module-start-new-conversation'
) as any;
);
if (startItem && startItem.focus) {
startItem.focus();
return;
}
// tslint:disable-next-line no-unnecessary-type-assertion
const conversationItem = scrollContainer.querySelector(
const conversationItem: HTMLElement | null = scrollContainer.querySelector(
'.module-conversation-list-item'
) as any;
);
if (conversationItem && conversationItem.focus) {
conversationItem.focus();
return;
}
// tslint:disable-next-line no-unnecessary-type-assertion
const messageItem = scrollContainer.querySelector(
const messageItem: HTMLElement | null = scrollContainer.querySelector(
'.module-message-search-result'
) as any;
);
if (messageItem && messageItem.focus) {
messageItem.focus();
return;
}
};
public getScrollContainer = () => {
public getScrollContainer = (): HTMLDivElement | null => {
if (!this.listRef || !this.listRef.current) {
return;
return null;
}
const list = this.listRef.current;
if (!list.Grid || !list.Grid._scrollingContainer) {
return;
// We're using an internal variable (_scrollingContainer)) here,
// 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(
// tslint:disable-next-line cyclomatic-complexity
(data: OnScrollParamsType) => {
// Ignore scroll events generated as react-virtualized recursively scrolls and
// re-measures to get us where we want to go.
@ -308,9 +308,9 @@ export class SearchResults extends React.Component<PropsType, StateType> {
return;
}
const messageItems = scrollContainer.querySelectorAll(
const messageItems: NodeListOf<HTMLElement> = scrollContainer.querySelectorAll(
'.module-message-search-result'
) as any;
);
if (messageItems && messageItems.length > 0) {
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'
) as any;
);
if (contactItems && contactItems.length > 0) {
const last = contactItems[contactItems.length - 1];
@ -336,14 +336,12 @@ export class SearchResults extends React.Component<PropsType, StateType> {
const startItem = scrollContainer.querySelectorAll(
'.module-start-new-conversation'
) as any;
) as NodeListOf<HTMLElement>;
if (startItem && startItem.length > 0) {
const last = startItem[startItem.length - 1];
if (last && last.focus) {
last.focus();
return;
}
}
}
@ -352,7 +350,7 @@ export class SearchResults extends React.Component<PropsType, StateType> {
{ maxWait: 100 }
);
public renderRowContents(row: SearchResultRowType) {
public renderRowContents(row: SearchResultRowType): JSX.Element {
const {
searchTerm,
i18n,
@ -368,13 +366,15 @@ export class SearchResults extends React.Component<PropsType, StateType> {
onClick={this.handleStartNewConversation}
/>
);
} else if (row.type === 'sms-mms-not-supported-text') {
}
if (row.type === 'sms-mms-not-supported-text') {
return (
<div className="module-search-results__sms-not-supported">
{i18n('notSupportedSMS')}
</div>
);
} else if (row.type === 'conversations-header') {
}
if (row.type === 'conversations-header') {
return (
<div
className="module-search-results__conversations-header"
@ -384,7 +384,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
{i18n('conversationsHeader')}
</div>
);
} else if (row.type === 'conversation') {
}
if (row.type === 'conversation') {
const { data } = row;
return (
@ -395,7 +396,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
i18n={i18n}
/>
);
} else if (row.type === 'contacts-header') {
}
if (row.type === 'contacts-header') {
return (
<div
className="module-search-results__contacts-header"
@ -405,7 +407,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
{i18n('contactsHeader')}
</div>
);
} else if (row.type === 'contact') {
}
if (row.type === 'contact') {
const { data } = row;
return (
@ -416,7 +419,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
i18n={i18n}
/>
);
} else if (row.type === 'messages-header') {
}
if (row.type === 'messages-header') {
return (
<div
className="module-search-results__messages-header"
@ -426,21 +430,22 @@ export class SearchResults extends React.Component<PropsType, StateType> {
{i18n('messagesHeader')}
</div>
);
} else if (row.type === 'message') {
}
if (row.type === 'message') {
const { data } = row;
return renderMessageSearchResult(data);
} else if (row.type === 'spinner') {
}
if (row.type === 'spinner') {
return (
<div className="module-search-results__spinner-container">
<Spinner size="24px" svgSize="small" />
</div>
);
} else {
throw new Error(
'SearchResults.renderRowContents: Encountered unknown row type'
);
}
throw new Error(
'SearchResults.renderRowContents: Encountered unknown row type'
);
}
public renderRow = ({
@ -469,7 +474,7 @@ export class SearchResults extends React.Component<PropsType, StateType> {
);
};
public componentDidUpdate(prevProps: PropsType) {
public componentDidUpdate(prevProps: PropsType): void {
const {
items,
searchTerm,
@ -493,9 +498,9 @@ export class SearchResults extends React.Component<PropsType, StateType> {
}
}
public getList = () => {
public getList = (): List | null => {
if (!this.listRef) {
return;
return null;
}
const { current } = this.listRef;
@ -503,7 +508,7 @@ export class SearchResults extends React.Component<PropsType, StateType> {
return current;
};
public recomputeRowHeights = (row?: number) => {
public recomputeRowHeights = (row?: number): void => {
const list = this.getList();
if (!list) {
return;
@ -512,18 +517,18 @@ export class SearchResults extends React.Component<PropsType, StateType> {
list.recomputeRowHeights(row);
};
public resizeAll = () => {
public resizeAll = (): void => {
this.cellSizeCache.clearAll();
this.recomputeRowHeights(0);
};
public getRowCount() {
public getRowCount(): number {
const { items } = this.props;
return items ? items.length : 0;
}
public render() {
public render(): JSX.Element {
const {
height,
i18n,
@ -574,7 +579,7 @@ export class SearchResults extends React.Component<PropsType, StateType> {
<div
className="module-search-results"
aria-live="polite"
role="group"
role="presentation"
tabIndex={-1}
ref={this.containerRef}
onKeyDown={this.handleKeyDown}
@ -592,6 +597,8 @@ export class SearchResults extends React.Component<PropsType, StateType> {
rowRenderer={this.renderRow}
scrollToIndex={scrollToIndex}
tabIndex={-1}
// TODO: DESKTOP-687
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onScroll={this.onScroll as any}
width={width}
/>

View File

@ -3,12 +3,8 @@ import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import { boolean, select } from '@storybook/addon-knobs';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { Props, ShortcutGuide } from './ShortcutGuide';
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 { i18n, close, hasInstalledStickers, platform } = props;
const isMacOS = platform === 'darwin';
@ -211,9 +211,11 @@ export const ShortcutGuide = (props: Props) => {
{i18n('Keyboard--header')}
</div>
<button
aria-label={i18n('close-popup')}
className="module-shortcut-guide__header-close"
onClick={close}
title={i18n('close-popup')}
type="button"
/>
</div>
<div
@ -282,17 +284,17 @@ function renderShortcut(
i18n: LocalizerType
) {
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">
{i18n(shortcut.description)}
</div>
<div className="module-shortcut-guide__shortcut__key-container">
{shortcut.keys.map((keys, outerIndex) => (
{shortcut.keys.map(keys => (
<div
key={outerIndex}
key={`${shortcut.description}--${keys.map(k => k).join('-')}`}
className="module-shortcut-guide__shortcut__key-inner-container"
>
{keys.map((key, mapIndex) => {
{keys.map(key => {
let label: string = key;
let isSquare = true;
@ -334,7 +336,7 @@ function renderShortcut(
return (
<span
key={mapIndex}
key={`shortcut__key--${key}`}
className={classNames(
'module-shortcut-guide__shortcut__key',
isSquare

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,11 @@
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 { boolean, select } from '@storybook/addon-knobs';
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);

View File

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

View File

@ -1,6 +1,4 @@
// A separate file so this doesn't get picked up by StyleGuidist over real components
import { Ref } from 'react';
import { MutableRefObject, Ref } from 'react';
import { isFunction } from 'lodash';
import memoizee from 'memoizee';
@ -8,6 +6,8 @@ export function cleanId(id: string): string {
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 = () =>
memoizee(
<T>(...refs: Array<Ref<T>>) => {
@ -16,8 +16,9 @@ export const createRefMerger = () =>
if (isFunction(r)) {
r(t);
} else if (r) {
// @ts-ignore: React's typings for ref objects is annoying
r.current = t;
// Using a MutableRefObject as intended
// 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);
});
// eslint-disable-next-line no-console
console.log('Preloading emoji images');
window.log.info('Preloading emoji images');
const start = Date.now();
data.forEach(emoji => {
@ -127,8 +126,7 @@ export const preloadImages = async (): Promise<void> => {
await imageQueue.onEmpty();
const end = Date.now();
// eslint-disable-next-line no-console
console.log(`Done preloading emoji images in ${end - start}ms`);
window.log.info(`Done preloading emoji images in ${end - start}ms`);
};
const dataByShortName = keyBy(data, 'short_name');

View File

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

View File

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