Migrate components to eslint
This commit is contained in:
parent
de66486e41
commit
b13dbcfa77
69 changed files with 875 additions and 800 deletions
|
@ -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
|
||||
|
|
18
.eslintrc.js
18
.eslintrc.js
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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' }}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 */
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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/**",
|
||||
|
|
Loading…
Reference in a new issue