Migrate conversations to ESLint

This commit is contained in:
Chris Svenningsen 2020-09-14 12:51:27 -07:00 committed by Josh Perez
parent b4f0f3c685
commit 372aa44e49
90 changed files with 1261 additions and 1165 deletions

View file

@ -33,7 +33,8 @@ webpack.config.ts
sticker-creator/**/*.ts sticker-creator/**/*.ts
sticker-creator/**/*.tsx sticker-creator/**/*.tsx
ts/*.ts ts/*.ts
ts/components/conversation/** ts/components/*.ts
ts/components/*.tsx
ts/components/stickers/** ts/components/stickers/**
ts/shims/** ts/shims/**
ts/sql/** ts/sql/**

View file

@ -118,6 +118,8 @@ module.exports = {
rules: { rules: {
...rules, ...rules,
'import/no-extraneous-dependencies': 'off', 'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
'react/no-array-index-key': 'off',
}, },
}, },
], ],

View file

@ -151,6 +151,10 @@
"message": "Set Up as Standalone Device", "message": "Set Up as Standalone Device",
"description": "Only available on development modes, menu option to open up the standalone device setup sequence" "description": "Only available on development modes, menu option to open up the standalone device setup sequence"
}, },
"messageContextMenuButton": {
"message": "More actions",
"description": "Label for context button next to each message"
},
"contextMenuCopyLink": { "contextMenuCopyLink": {
"message": "Copy Link", "message": "Copy Link",
"description": "Shown in the context menu for a link to indicate that the user can copy the link" "description": "Shown in the context menu for a link to indicate that the user can copy the link"
@ -985,6 +989,10 @@
"theirIdentityUnknown": { "theirIdentityUnknown": {
"message": "You haven't exchanged any messages with this contact yet. Your safety number with them will be available after the first message." "message": "You haven't exchanged any messages with this contact yet. Your safety number with them will be available after the first message."
}, },
"goBack": {
"message": "Go back",
"description": "Label for back button in a conversation"
},
"moreInfo": { "moreInfo": {
"message": "More Info...", "message": "More Info...",
"description": "Shown on the drop-down menu for an individual message, takes you to message detail screen" "description": "Shown on the drop-down menu for an individual message, takes you to message detail screen"
@ -2772,6 +2780,14 @@
"message": "Ringing...", "message": "Ringing...",
"description": "Shown in the call screen when placing an outgoing call that is now ringing" "description": "Shown in the call screen when placing an outgoing call that is now ringing"
}, },
"makeOutgoingCall": {
"message": "Start a call",
"description": "Title for the call button in a conversation"
},
"makeOutgoingVideoCall": {
"message": "Start a video call",
"description": "Title for the video call button in a conversation"
},
"callReconnecting": { "callReconnecting": {
"message": "Reconnecting...", "message": "Reconnecting...",
"description": "Shown in the call screen when the call is reconnecting due to network issues" "description": "Shown in the call screen when the call is reconnecting due to network issues"
@ -3574,7 +3590,7 @@
} }
}, },
"close": { "close": {
"message": "close", "message": "Close",
"description": "Generic close label" "description": "Generic close label"
}, },
"previous": { "previous": {

View file

@ -13,15 +13,20 @@ export class AddNewLines extends React.Component<Props> {
renderNonNewLine: ({ text }) => text, renderNonNewLine: ({ text }) => text,
}; };
public render() { public render():
| JSX.Element
| string
| null
| Array<JSX.Element | string | null> {
const { text, renderNonNewLine } = this.props; const { text, renderNonNewLine } = this.props;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const results: Array<any> = []; const results: Array<any> = [];
const FIND_NEWLINES = /\n/g; const FIND_NEWLINES = /\n/g;
// We have to do this, because renderNonNewLine is not required in our Props object, // We have to do this, because renderNonNewLine is not required in our Props object,
// but it is always provided via defaultProps. // but it is always provided via defaultProps.
if (!renderNonNewLine) { if (!renderNonNewLine) {
return; return null;
} }
let match = FIND_NEWLINES.exec(text); let match = FIND_NEWLINES.exec(text);
@ -35,20 +40,20 @@ export class AddNewLines extends React.Component<Props> {
while (match) { while (match) {
if (last < match.index) { if (last < match.index) {
const textWithNoNewline = text.slice(last, match.index); const textWithNoNewline = text.slice(last, match.index);
results.push( count += 1;
renderNonNewLine({ text: textWithNoNewline, key: count++ }) results.push(renderNonNewLine({ text: textWithNoNewline, key: count }));
);
} }
results.push(<br key={count++} />); count += 1;
results.push(<br key={count} />);
// @ts-ignore
last = FIND_NEWLINES.lastIndex; last = FIND_NEWLINES.lastIndex;
match = FIND_NEWLINES.exec(text); match = FIND_NEWLINES.exec(text);
} }
if (last < text.length) { if (last < text.length) {
results.push(renderNonNewLine({ text: text.slice(last), key: count++ })); count += 1;
results.push(renderNonNewLine({ text: text.slice(last), key: count }));
} }
return results; return results;

View file

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

View file

@ -27,18 +27,14 @@ export interface Props {
const IMAGE_WIDTH = 120; const IMAGE_WIDTH = 120;
const IMAGE_HEIGHT = 120; const IMAGE_HEIGHT = 120;
export class AttachmentList extends React.Component<Props> { export const AttachmentList = ({
// tslint:disable-next-line max-func-body-length */
public render() {
const {
attachments, attachments,
i18n, i18n,
onAddAttachment, onAddAttachment,
onClickAttachment, onClickAttachment,
onCloseAttachment, onCloseAttachment,
onClose, onClose,
} = this.props; }: Props): JSX.Element | null => {
if (!attachments.length) { if (!attachments.length) {
return null; return null;
} }
@ -50,8 +46,10 @@ export class AttachmentList extends React.Component<Props> {
{attachments.length > 1 ? ( {attachments.length > 1 ? (
<div className="module-attachments__header"> <div className="module-attachments__header">
<button <button
type="button"
onClick={onClose} onClick={onClose}
className="module-attachments__close-button" className="module-attachments__close-button"
aria-label={i18n('close')}
/> />
</div> </div>
) : null} ) : null}
@ -62,8 +60,7 @@ export class AttachmentList extends React.Component<Props> {
isImageTypeSupported(contentType) || isImageTypeSupported(contentType) ||
isVideoTypeSupported(contentType) isVideoTypeSupported(contentType)
) { ) {
const imageKey = const imageKey = getUrl(attachment) || attachment.fileName || index;
getUrl(attachment) || attachment.fileName || index;
const clickCallback = const clickCallback =
attachments.length > 1 ? onClickAttachment : undefined; attachments.length > 1 ? onClickAttachment : undefined;
@ -75,12 +72,12 @@ export class AttachmentList extends React.Component<Props> {
])} ])}
i18n={i18n} i18n={i18n}
attachment={attachment} attachment={attachment}
softCorners={true} softCorners
playIconOverlay={isVideoAttachment(attachment)} playIconOverlay={isVideoAttachment(attachment)}
height={IMAGE_HEIGHT} height={IMAGE_HEIGHT}
width={IMAGE_WIDTH} width={IMAGE_WIDTH}
url={getUrl(attachment)} url={getUrl(attachment)}
closeButton={true} closeButton
onClick={clickCallback} onClick={clickCallback}
onClickClose={onCloseAttachment} onClickClose={onCloseAttachment}
onError={() => { onError={() => {
@ -90,8 +87,7 @@ export class AttachmentList extends React.Component<Props> {
); );
} }
const genericKey = const genericKey = getUrl(attachment) || attachment.fileName || index;
getUrl(attachment) || attachment.fileName || index;
return ( return (
<StagedGenericAttachment <StagedGenericAttachment
@ -103,13 +99,9 @@ export class AttachmentList extends React.Component<Props> {
); );
})} })}
{allVisualAttachments ? ( {allVisualAttachments ? (
<StagedPlaceholderAttachment <StagedPlaceholderAttachment onClick={onAddAttachment} i18n={i18n} />
onClick={onAddAttachment}
i18n={i18n}
/>
) : null} ) : null}
</div> </div>
</div> </div>
); );
} };
}

View file

@ -31,37 +31,30 @@ export function getCallingNotificationText(
if (wasDeclined) { if (wasDeclined) {
if (wasVideoCall) { if (wasVideoCall) {
return i18n('declinedIncomingVideoCall'); return i18n('declinedIncomingVideoCall');
} else { }
return i18n('declinedIncomingAudioCall'); return i18n('declinedIncomingAudioCall');
} }
} else if (wasAccepted) { if (wasAccepted) {
if (wasVideoCall) { if (wasVideoCall) {
return i18n('acceptedIncomingVideoCall'); return i18n('acceptedIncomingVideoCall');
} else { }
return i18n('acceptedIncomingAudioCall'); return i18n('acceptedIncomingAudioCall');
} }
} else {
if (wasVideoCall) { if (wasVideoCall) {
return i18n('missedIncomingVideoCall'); return i18n('missedIncomingVideoCall');
} else { }
return i18n('missedIncomingAudioCall'); return i18n('missedIncomingAudioCall');
} }
}
} else {
if (wasAccepted) { if (wasAccepted) {
if (wasVideoCall) { if (wasVideoCall) {
return i18n('acceptedOutgoingVideoCall'); return i18n('acceptedOutgoingVideoCall');
} else { }
return i18n('acceptedOutgoingAudioCall'); return i18n('acceptedOutgoingAudioCall');
} }
} else {
if (wasVideoCall) { if (wasVideoCall) {
return i18n('missedOrDeclinedOutgoingVideoCall'); return i18n('missedOrDeclinedOutgoingVideoCall');
} else { }
return i18n('missedOrDeclinedOutgoingAudioCall'); return i18n('missedOrDeclinedOutgoingAudioCall');
}
}
}
} }
export const CallingNotification = (props: Props): JSX.Element | null => { export const CallingNotification = (props: Props): JSX.Element | null => {
@ -81,7 +74,7 @@ export const CallingNotification = (props: Props): JSX.Element | null => {
<Timestamp <Timestamp
i18n={i18n} i18n={i18n}
timestamp={acceptedTime || endedTime} timestamp={acceptedTime || endedTime}
extended={true} extended
direction="outgoing" direction="outgoing"
withImageNoCaption={false} withImageNoCaption={false}
withSticker={false} withSticker={false}

View file

@ -6,11 +6,9 @@ import { storiesOf } from '@storybook/react';
import { ContactDetail, Props } from './ContactDetail'; import { ContactDetail, Props } from './ContactDetail';
import { AddressType, ContactFormType } from '../../types/Contact'; import { AddressType, ContactFormType } from '../../types/Contact';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/ContactDetail', module); const story = storiesOf('Components/Conversation/ContactDetail', module);

View file

@ -72,6 +72,7 @@ function getLabelForAddress(
} }
export class ContactDetail extends React.Component<Props> { export class ContactDetail extends React.Component<Props> {
// eslint-disable-next-line class-methods-use-this
public renderSendMessage({ public renderSendMessage({
hasSignalAccount, hasSignalAccount,
i18n, i18n,
@ -80,20 +81,24 @@ export class ContactDetail extends React.Component<Props> {
hasSignalAccount: boolean; hasSignalAccount: boolean;
i18n: (key: string, values?: Array<string>) => string; i18n: (key: string, values?: Array<string>) => string;
onSendMessage: () => void; onSendMessage: () => void;
}) { }): JSX.Element | null {
if (!hasSignalAccount) { if (!hasSignalAccount) {
return null; return null;
} }
// We don't want the overall click handler for this element to fire, so we stop // We don't want the overall click handler for this element to fire, so we stop
// propagation before handing control to the caller's callback. // propagation before handing control to the caller's callback.
const onClick = (e: React.MouseEvent<{}>): void => { const onClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
e.stopPropagation(); e.stopPropagation();
onSendMessage(); onSendMessage();
}; };
return ( return (
<button className="module-contact-detail__send-message" onClick={onClick}> <button
type="button"
className="module-contact-detail__send-message"
onClick={onClick}
>
<div className="module-contact-detail__send-message__inner"> <div className="module-contact-detail__send-message__inner">
<div className="module-contact-detail__send-message__bubble-icon" /> <div className="module-contact-detail__send-message__bubble-icon" />
{i18n('sendMessageToContact')} {i18n('sendMessageToContact')}
@ -102,9 +107,13 @@ export class ContactDetail extends React.Component<Props> {
); );
} }
public renderEmail(items: Array<Email> | undefined, i18n: LocalizerType) { // eslint-disable-next-line class-methods-use-this
public renderEmail(
items: Array<Email> | undefined,
i18n: LocalizerType
): Array<JSX.Element> | undefined {
if (!items || items.length === 0) { if (!items || items.length === 0) {
return; return undefined;
} }
return items.map((item: Email) => { return items.map((item: Email) => {
@ -122,9 +131,13 @@ export class ContactDetail extends React.Component<Props> {
}); });
} }
public renderPhone(items: Array<Phone> | undefined, i18n: LocalizerType) { // eslint-disable-next-line class-methods-use-this
public renderPhone(
items: Array<Phone> | undefined,
i18n: LocalizerType
): Array<JSX.Element> | null | undefined {
if (!items || items.length === 0) { if (!items || items.length === 0) {
return; return undefined;
} }
return items.map((item: Phone) => { return items.map((item: Phone) => {
@ -142,15 +155,20 @@ export class ContactDetail extends React.Component<Props> {
}); });
} }
public renderAddressLine(value: string | undefined) { // eslint-disable-next-line class-methods-use-this
public renderAddressLine(value: string | undefined): JSX.Element | undefined {
if (!value) { if (!value) {
return; return undefined;
} }
return <div>{value}</div>; return <div>{value}</div>;
} }
public renderPOBox(poBox: string | undefined, i18n: LocalizerType) { // eslint-disable-next-line class-methods-use-this
public renderPOBox(
poBox: string | undefined,
i18n: LocalizerType
): JSX.Element | null {
if (!poBox) { if (!poBox) {
return null; return null;
} }
@ -162,7 +180,8 @@ export class ContactDetail extends React.Component<Props> {
); );
} }
public renderAddressLineTwo(address: PostalAddress) { // eslint-disable-next-line class-methods-use-this
public renderAddressLineTwo(address: PostalAddress): JSX.Element | null {
if (address.city || address.region || address.postcode) { if (address.city || address.region || address.postcode) {
return ( return (
<div> <div>
@ -177,13 +196,14 @@ export class ContactDetail extends React.Component<Props> {
public renderAddresses( public renderAddresses(
addresses: Array<PostalAddress> | undefined, addresses: Array<PostalAddress> | undefined,
i18n: LocalizerType i18n: LocalizerType
) { ): Array<JSX.Element> | undefined {
if (!addresses || addresses.length === 0) { if (!addresses || addresses.length === 0) {
return; return undefined;
} }
return addresses.map((address: PostalAddress, index: number) => { return addresses.map((address: PostalAddress, index: number) => {
return ( return (
// eslint-disable-next-line react/no-array-index-key
<div key={index} className="module-contact-detail__additional-contact"> <div key={index} className="module-contact-detail__additional-contact">
<div className="module-contact-detail__additional-contact__type"> <div className="module-contact-detail__additional-contact__type">
{getLabelForAddress(address, i18n)} {getLabelForAddress(address, i18n)}
@ -198,7 +218,7 @@ export class ContactDetail extends React.Component<Props> {
}); });
} }
public render() { public render(): JSX.Element {
const { contact, hasSignalAccount, i18n, onSendMessage } = this.props; const { contact, hasSignalAccount, i18n, onSendMessage } = this.props;
const isIncoming = false; const isIncoming = false;
const module = 'contact-detail'; const module = 'contact-detail';

View file

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

View file

@ -12,15 +12,12 @@ export interface PropsType {
profileName?: string; profileName?: string;
} }
export class ContactName extends React.Component<PropsType> { export const ContactName = ({ module, title }: PropsType): JSX.Element => {
public render() { const prefix = module || 'module-contact-name';
const { module, title } = this.props;
const prefix = module ? module : 'module-contact-name';
return ( return (
<span className={prefix} dir="auto"> <span className={prefix} dir="auto">
<Emojify text={title || ''} /> <Emojify text={title || ''} />
</span> </span>
); );
} };
}

View file

@ -3,18 +3,14 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; 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 { import {
ConversationHeader, ConversationHeader,
PropsActionsType, PropsActionsType,
PropsHousekeepingType, PropsHousekeepingType,
PropsType, PropsType,
} from './ConversationHeader'; } from './ConversationHeader';
import { gifUrl } from '../../storybook/Fixtures'; import { gifUrl } from '../../storybook/Fixtures';
const book = storiesOf('Components/Conversation/ConversationHeader', module); const book = storiesOf('Components/Conversation/ConversationHeader', module);

View file

@ -71,6 +71,9 @@ export type PropsType = PropsDataType &
export class ConversationHeader extends React.Component<PropsType> { export class ConversationHeader extends React.Component<PropsType> {
public showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void; public showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void;
// Comes from a third-party dependency
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public menuTriggerRef: React.RefObject<any>; public menuTriggerRef: React.RefObject<any>;
public constructor(props: PropsType) { public constructor(props: PropsType) {
@ -80,28 +83,30 @@ export class ConversationHeader extends React.Component<PropsType> {
this.showMenuBound = this.showMenu.bind(this); this.showMenuBound = this.showMenu.bind(this);
} }
public showMenu(event: React.MouseEvent<HTMLButtonElement>) { public showMenu(event: React.MouseEvent<HTMLButtonElement>): void {
if (this.menuTriggerRef.current) { if (this.menuTriggerRef.current) {
this.menuTriggerRef.current.handleContextClick(event); this.menuTriggerRef.current.handleContextClick(event);
} }
} }
public renderBackButton() { public renderBackButton(): JSX.Element {
const { onGoBack, showBackButton } = this.props; const { i18n, onGoBack, showBackButton } = this.props;
return ( return (
<button <button
type="button"
onClick={onGoBack} onClick={onGoBack}
className={classNames( className={classNames(
'module-conversation-header__back-icon', 'module-conversation-header__back-icon',
showBackButton ? 'module-conversation-header__back-icon--show' : null showBackButton ? 'module-conversation-header__back-icon--show' : null
)} )}
disabled={!showBackButton} disabled={!showBackButton}
aria-label={i18n('goBack')}
/> />
); );
} }
public renderTitle() { public renderTitle(): JSX.Element {
const { const {
name, name,
phoneNumber, phoneNumber,
@ -145,7 +150,7 @@ export class ConversationHeader extends React.Component<PropsType> {
); );
} }
public renderAvatar() { public renderAvatar(): JSX.Element {
const { const {
avatarPath, avatarPath,
color, color,
@ -176,7 +181,7 @@ export class ConversationHeader extends React.Component<PropsType> {
); );
} }
public renderExpirationLength() { public renderExpirationLength(): JSX.Element | null {
const { expirationSettingName, showBackButton } = this.props; const { expirationSettingName, showBackButton } = this.props;
if (!expirationSettingName) { if (!expirationSettingName) {
@ -200,12 +205,13 @@ export class ConversationHeader extends React.Component<PropsType> {
); );
} }
public renderMoreButton(triggerId: string) { public renderMoreButton(triggerId: string): JSX.Element {
const { showBackButton } = this.props; const { i18n, showBackButton } = this.props;
return ( return (
<ContextMenuTrigger id={triggerId} ref={this.menuTriggerRef}> <ContextMenuTrigger id={triggerId} ref={this.menuTriggerRef}>
<button <button
type="button"
onClick={this.showMenuBound} onClick={this.showMenuBound}
className={classNames( className={classNames(
'module-conversation-header__more-button', 'module-conversation-header__more-button',
@ -214,16 +220,18 @@ export class ConversationHeader extends React.Component<PropsType> {
: 'module-conversation-header__more-button--show' : 'module-conversation-header__more-button--show'
)} )}
disabled={showBackButton} disabled={showBackButton}
aria-label={i18n('moreInfo')}
/> />
</ContextMenuTrigger> </ContextMenuTrigger>
); );
} }
public renderSearchButton() { public renderSearchButton(): JSX.Element {
const { onSearchInConversation, showBackButton } = this.props; const { i18n, onSearchInConversation, showBackButton } = this.props;
return ( return (
<button <button
type="button"
onClick={onSearchInConversation} onClick={onSearchInConversation}
className={classNames( className={classNames(
'module-conversation-header__search-button', 'module-conversation-header__search-button',
@ -232,22 +240,31 @@ export class ConversationHeader extends React.Component<PropsType> {
: 'module-conversation-header__search-button--show' : 'module-conversation-header__search-button--show'
)} )}
disabled={showBackButton} disabled={showBackButton}
aria-label={i18n('search')}
/> />
); );
} }
public renderOutgoingAudioCallButton() { public renderOutgoingAudioCallButton(): JSX.Element | null {
if (!window.CALLING) { if (!window.CALLING) {
return null; return null;
} }
if (this.props.type === 'group' || this.props.isMe) {
const {
i18n,
isMe,
onOutgoingAudioCallInConversation,
showBackButton,
type,
} = this.props;
if (type === 'group' || isMe) {
return null; return null;
} }
const { onOutgoingAudioCallInConversation, showBackButton } = this.props;
return ( return (
<button <button
type="button"
onClick={onOutgoingAudioCallInConversation} onClick={onOutgoingAudioCallInConversation}
className={classNames( className={classNames(
'module-conversation-header__audio-calling-button', 'module-conversation-header__audio-calling-button',
@ -256,15 +273,19 @@ export class ConversationHeader extends React.Component<PropsType> {
: 'module-conversation-header__audio-calling-button--show' : 'module-conversation-header__audio-calling-button--show'
)} )}
disabled={showBackButton} disabled={showBackButton}
aria-label={i18n('makeOutgoingCall')}
/> />
); );
} }
public renderOutgoingVideoCallButton() { public renderOutgoingVideoCallButton(): JSX.Element | null {
if (!window.CALLING) { if (!window.CALLING) {
return null; return null;
} }
if (this.props.type === 'group' || this.props.isMe) {
const { i18n, isMe, type } = this.props;
if (type === 'group' || isMe) {
return null; return null;
} }
@ -272,6 +293,7 @@ export class ConversationHeader extends React.Component<PropsType> {
return ( return (
<button <button
type="button"
onClick={onOutgoingVideoCallInConversation} onClick={onOutgoingVideoCallInConversation}
className={classNames( className={classNames(
'module-conversation-header__video-calling-button', 'module-conversation-header__video-calling-button',
@ -280,11 +302,12 @@ export class ConversationHeader extends React.Component<PropsType> {
: 'module-conversation-header__video-calling-button--show' : 'module-conversation-header__video-calling-button--show'
)} )}
disabled={showBackButton} disabled={showBackButton}
aria-label={i18n('makeOutgoingVideoCall')}
/> />
); );
} }
public renderMenu(triggerId: string) { public renderMenu(triggerId: string): JSX.Element {
const { const {
disableTimerChanges, disableTimerChanges,
i18n, i18n,
@ -323,7 +346,9 @@ export class ConversationHeader extends React.Component<PropsType> {
} }
muteOptions.push(...getMuteOptions(i18n)); muteOptions.push(...getMuteOptions(i18n));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const disappearingTitle = i18n('disappearingMessages') as any; const disappearingTitle = i18n('disappearingMessages') as any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const muteTitle = i18n('muteNotificationsTitle') as any; const muteTitle = i18n('muteNotificationsTitle') as any;
const isGroup = type === 'group'; const isGroup = type === 'group';
@ -382,7 +407,7 @@ export class ConversationHeader extends React.Component<PropsType> {
); );
} }
public render() { public render(): JSX.Element {
const { id } = this.props; const { id } = this.props;
const triggerId = `conversation-${id}`; const triggerId = `conversation-${id}`;

View file

@ -1,11 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { number as numberKnob, text } from '@storybook/addon-knobs'; import { number as numberKnob, text } from '@storybook/addon-knobs';
import { ConversationHero } from './ConversationHero';
// @ts-ignore import { ConversationHero } from './ConversationHero';
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -187,7 +185,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<ConversationHero <ConversationHero
i18n={i18n} i18n={i18n}
isMe={true} isMe
title={getTitle()} title={getTitle()}
conversationType="direct" conversationType="direct"
phoneNumber={getPhoneNumber()} phoneNumber={getPhoneNumber()}

View file

@ -35,6 +35,8 @@ const renderMembershipRow = ({
sharedGroupNames.length > 0 sharedGroupNames.length > 0
) { ) {
const firstThreeGroups = take(sharedGroupNames, 3).map((group, i) => ( const firstThreeGroups = take(sharedGroupNames, 3).map((group, i) => (
// We cannot guarantee uniqueness of group names
// eslint-disable-next-line react/no-array-index-key
<strong key={i} className={nameClassName}> <strong key={i} className={nameClassName}>
<Emojify text={group} /> <Emojify text={group} />
</strong> </strong>
@ -56,7 +58,8 @@ const renderMembershipRow = ({
/> />
</div> </div>
); );
} else if (firstThreeGroups.length === 3) { }
if (firstThreeGroups.length === 3) {
return ( return (
<div className={className}> <div className={className}>
<Intl <Intl
@ -70,7 +73,8 @@ const renderMembershipRow = ({
/> />
</div> </div>
); );
} else if (firstThreeGroups.length >= 2) { }
if (firstThreeGroups.length >= 2) {
return ( return (
<div className={className}> <div className={className}>
<Intl <Intl
@ -83,7 +87,8 @@ const renderMembershipRow = ({
/> />
</div> </div>
); );
} else if (firstThreeGroups.length >= 1) { }
if (firstThreeGroups.length >= 1) {
return ( return (
<div className={className}> <div className={className}>
<Intl <Intl
@ -115,9 +120,11 @@ export const ConversationHero = ({
title, title,
onHeightChange, onHeightChange,
updateSharedGroups, updateSharedGroups,
}: Props) => { }: Props): JSX.Element => {
const firstRenderRef = React.useRef(true); const firstRenderRef = React.useRef(true);
// TODO: DESKTOP-686
/* eslint-disable react-hooks/exhaustive-deps */
React.useEffect(() => { React.useEffect(() => {
// If any of the depenencies for this hook change then the height of this // If any of the depenencies for this hook change then the height of this
// component may have changed. The cleanup function notifies listeners of // component may have changed. The cleanup function notifies listeners of
@ -144,11 +151,13 @@ export const ConversationHero = ({
`pn-${profileName}`, `pn-${profileName}`,
sharedGroupNames.map(g => `g-${g}`).join(' '), sharedGroupNames.map(g => `g-${g}`).join(' '),
]); ]);
/* eslint-enable react-hooks/exhaustive-deps */
const phoneNumberOnly = Boolean( const phoneNumberOnly = Boolean(
!name && !profileName && conversationType === 'direct' !name && !profileName && conversationType === 'direct'
); );
/* eslint-disable no-nested-ternary */
return ( return (
<div className="module-conversation-hero"> <div className="module-conversation-hero">
<Avatar <Avatar
@ -190,4 +199,5 @@ export const ConversationHero = ({
{renderMembershipRow({ isMe, sharedGroupNames, conversationType, i18n })} {renderMembershipRow({ isMe, sharedGroupNames, conversationType, i18n })}
</div> </div>
); );
/* eslint-enable no-nested-ternary */
}; };

View file

@ -5,12 +5,10 @@ import { boolean, number } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { EmbeddedContact, Props } from './EmbeddedContact'; import { EmbeddedContact, Props } from './EmbeddedContact';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { ContactFormType } from '../../types/Contact'; import { ContactFormType } from '../../types/Contact';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/EmbeddedContact', module); const story = storiesOf('Components/Conversation/EmbeddedContact', module);

View file

@ -21,7 +21,7 @@ export interface Props {
} }
export class EmbeddedContact extends React.Component<Props> { export class EmbeddedContact extends React.Component<Props> {
public render() { public render(): JSX.Element {
const { const {
contact, contact,
i18n, i18n,
@ -36,6 +36,7 @@ export class EmbeddedContact extends React.Component<Props> {
return ( return (
<button <button
type="button"
className={classNames( className={classNames(
'module-embedded-contact', 'module-embedded-contact',
`module-embedded-contact--${direction}`, `module-embedded-contact--${direction}`,

View file

@ -13,7 +13,7 @@ function getImageTag({
sizeClass, sizeClass,
key, key,
}: { }: {
match: any; match: RegExpExecArray;
sizeClass?: SizeClassType; sizeClass?: SizeClassType;
key: string | number; key: string | number;
}) { }) {
@ -24,7 +24,6 @@ function getImageTag({
} }
return ( return (
// tslint:disable-next-line react-a11y-img-has-alt
<img <img
key={key} key={key}
src={img} src={img}
@ -48,15 +47,20 @@ export class Emojify extends React.Component<Props> {
renderNonEmoji: ({ text }) => text, renderNonEmoji: ({ text }) => text,
}; };
public render() { public render():
| JSX.Element
| string
| null
| Array<JSX.Element | string | null> {
const { text, sizeClass, renderNonEmoji } = this.props; const { text, sizeClass, renderNonEmoji } = this.props;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const results: Array<any> = []; const results: Array<any> = [];
const regex = emojiRegex(); const regex = emojiRegex();
// We have to do this, because renderNonEmoji is not required in our Props object, // We have to do this, because renderNonEmoji is not required in our Props object,
// but it is always provided via defaultProps. // but it is always provided via defaultProps.
if (!renderNonEmoji) { if (!renderNonEmoji) {
return; return null;
} }
let match = regex.exec(text); let match = regex.exec(text);
@ -70,17 +74,20 @@ export class Emojify extends React.Component<Props> {
while (match) { while (match) {
if (last < match.index) { if (last < match.index) {
const textWithNoEmoji = text.slice(last, match.index); const textWithNoEmoji = text.slice(last, match.index);
results.push(renderNonEmoji({ text: textWithNoEmoji, key: count++ })); count += 1;
results.push(renderNonEmoji({ text: textWithNoEmoji, key: count }));
} }
results.push(getImageTag({ match, sizeClass, key: count++ })); count += 1;
results.push(getImageTag({ match, sizeClass, key: count }));
last = regex.lastIndex; last = regex.lastIndex;
match = regex.exec(text); match = regex.exec(text);
} }
if (last < text.length) { if (last < text.length) {
results.push(renderNonEmoji({ text: text.slice(last), key: count++ })); count += 1;
results.push(renderNonEmoji({ text: text.slice(last), key: count }));
} }
return results; return results;

View file

@ -13,7 +13,7 @@ export interface Props {
} }
export class ExpireTimer extends React.Component<Props> { export class ExpireTimer extends React.Component<Props> {
private interval: any; private interval: NodeJS.Timeout | null;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -21,26 +21,28 @@ export class ExpireTimer extends React.Component<Props> {
this.interval = null; this.interval = null;
} }
public componentDidMount() { public componentDidMount(): void {
const { expirationLength } = this.props; const { expirationLength } = this.props;
const increment = getIncrement(expirationLength); const increment = getIncrement(expirationLength);
const updateFrequency = Math.max(increment, 500); const updateFrequency = Math.max(increment, 500);
const update = () => { const update = () => {
this.setState({ this.setState({
// Used to trigger renders
// eslint-disable-next-line react/no-unused-state
lastUpdated: Date.now(), lastUpdated: Date.now(),
}); });
}; };
this.interval = setInterval(update, updateFrequency); this.interval = setInterval(update, updateFrequency);
} }
public componentWillUnmount() { public componentWillUnmount(): void {
if (this.interval) { if (this.interval) {
clearInterval(this.interval); clearInterval(this.interval);
} }
} }
public render() { public render(): JSX.Element {
const { const {
direction, direction,
expirationLength, expirationLength,

View file

@ -33,7 +33,10 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping; export type Props = PropsData & PropsHousekeeping;
export class GroupNotification extends React.Component<Props> { export class GroupNotification extends React.Component<Props> {
public renderChange(change: Change, from: Contact) { public renderChange(
change: Change,
from: Contact
): JSX.Element | string | null | undefined {
const { contacts, type, newName } = change; const { contacts, type, newName } = change;
const { i18n } = this.props; const { i18n } = this.props;
@ -78,6 +81,7 @@ export class GroupNotification extends React.Component<Props> {
throw new Error('Group update is missing contacts'); throw new Error('Group update is missing contacts');
} }
// eslint-disable-next-line no-case-declarations
const otherPeopleNotifMsg = const otherPeopleNotifMsg =
otherPeople.length === 1 otherPeople.length === 1
? 'joinedTheGroup' ? 'joinedTheGroup'
@ -108,6 +112,7 @@ export class GroupNotification extends React.Component<Props> {
throw new Error('Group update is missing contacts'); throw new Error('Group update is missing contacts');
} }
// eslint-disable-next-line no-case-declarations
const leftKey = const leftKey =
contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup'; contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
@ -115,13 +120,14 @@ export class GroupNotification extends React.Component<Props> {
<Intl i18n={i18n} id={leftKey} components={[otherPeopleWithCommas]} /> <Intl i18n={i18n} id={leftKey} components={[otherPeopleWithCommas]} />
); );
case 'general': case 'general':
// eslint-disable-next-line consistent-return
return; return;
default: default:
throw missingCaseError(type); throw missingCaseError(type);
} }
} }
public render() { public render(): JSX.Element {
const { changes, i18n, from } = this.props; const { changes, i18n, from } = this.props;
// Leave messages are always from the person leaving, so we omit the fromLabel if // Leave messages are always from the person leaving, so we omit the fromLabel if
@ -153,8 +159,9 @@ export class GroupNotification extends React.Component<Props> {
<br /> <br />
</> </>
)} )}
{(changes || []).map((change, index) => ( {(changes || []).map((change, i) => (
<div key={index} className="module-group-notification__change"> // eslint-disable-next-line react/no-array-index-key
<div key={i} className="module-group-notification__change">
{this.renderChange(change, from)} {this.renderChange(change, from)}
</div> </div>
))} ))}

View file

@ -1,11 +1,9 @@
/* eslint-disable-next-line max-classes-per-file */
import * as React from 'react'; import * as React from 'react';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import { GroupV2ChangeType } from '../../groups'; import { GroupV2ChangeType } from '../../groups';
import { SmartContactRendererType } from '../../groupChange'; import { SmartContactRendererType } from '../../groupChange';
import { GroupV2Change } from './GroupV2Change'; import { GroupV2Change } from './GroupV2Change';
@ -19,17 +17,21 @@ const CONTACT_C = 'CONTACT_C';
const ADMIN_A = 'ADMIN_A'; const ADMIN_A = 'ADMIN_A';
const INVITEE_A = 'INVITEE_A'; const INVITEE_A = 'INVITEE_A';
// tslint:disable-next-line no-unnecessary-class
class AccessControlEnum { class AccessControlEnum {
static UNKNOWN = 0; static UNKNOWN = 0;
static ADMINISTRATOR = 1; static ADMINISTRATOR = 1;
static ANY = 2; static ANY = 2;
static MEMBER = 3; static MEMBER = 3;
} }
// tslint:disable-next-line no-unnecessary-class
class RoleEnum { class RoleEnum {
static UNKNOWN = 0; static UNKNOWN = 0;
static ADMINISTRATOR = 1; static ADMINISTRATOR = 1;
static DEFAULT = 2; static DEFAULT = 2;
} }
@ -468,7 +470,6 @@ storiesOf('Components/Conversation/GroupV2Change', module)
</> </>
); );
}) })
// tslint:disable-next-line max-func-body-length
.add('Member Privilege', () => { .add('Member Privilege', () => {
return ( return (
<> <>
@ -652,7 +653,6 @@ storiesOf('Components/Conversation/GroupV2Change', module)
</> </>
); );
}) })
// tslint:disable-next-line max-func-body-length
.add('Pending Remove - one', () => { .add('Pending Remove - one', () => {
return ( return (
<> <>

View file

@ -53,6 +53,8 @@ export function GroupV2Change(props: PropsType): React.ReactElement {
renderString: renderStringToIntl, renderString: renderStringToIntl,
RoleEnum, RoleEnum,
}).map((item: FullJSXType, index: number) => ( }).map((item: FullJSXType, index: number) => (
// Difficult to find a unique key for this type
// eslint-disable-next-line react/no-array-index-key
<div key={index}>{item}</div> <div key={index}>{item}</div>
))} ))}
</div> </div>

View file

@ -7,16 +7,13 @@ import { storiesOf } from '@storybook/react';
import { pngUrl } from '../../storybook/Fixtures'; import { pngUrl } from '../../storybook/Fixtures';
import { Image, Props } from './Image'; import { Image, Props } from './Image';
import { IMAGE_PNG } from '../../types/MIME'; import { IMAGE_PNG } from '../../types/MIME';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/Image', module); const story = storiesOf('Components/Conversation/Image', module);
// tslint:disable-next-line:cyclomatic-complexity
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
alt: text('alt', overrideProps.alt || ''), alt: text('alt', overrideProps.alt || ''),
attachment: overrideProps.attachment || { attachment: overrideProps.attachment || {
@ -170,6 +167,7 @@ story.add('Blurhash', () => {
const props = { const props = {
...defaultProps, ...defaultProps,
blurHash: 'thisisafakeblurhashthatwasmadeup', blurHash: 'thisisafakeblurhashthatwasmadeup',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
url: undefined as any, url: undefined as any,
}; };
@ -179,7 +177,9 @@ story.add('Missing Image', () => {
const defaultProps = createProps(); const defaultProps = createProps();
const props = { const props = {
...defaultProps, ...defaultProps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
attachment: undefined as any, attachment: undefined as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
url: undefined as any, url: undefined as any,
}; };

View file

@ -47,7 +47,7 @@ export class Image extends React.Component<Props> {
return Boolean(onClick && !pending && url); return Boolean(onClick && !pending && url);
} }
public handleClick = (event: React.MouseEvent) => { public handleClick = (event: React.MouseEvent): void => {
if (!this.canClick()) { if (!this.canClick()) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -65,7 +65,9 @@ export class Image extends React.Component<Props> {
} }
}; };
public handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => { public handleKeyDown = (
event: React.KeyboardEvent<HTMLButtonElement>
): void => {
if (!this.canClick()) { if (!this.canClick()) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -82,8 +84,7 @@ export class Image extends React.Component<Props> {
} }
}; };
// tslint:disable-next-line max-func-body-length cyclomatic-complexity public render(): JSX.Element {
public render() {
const { const {
alt, alt,
attachment, attachment,
@ -127,7 +128,10 @@ export class Image extends React.Component<Props> {
); );
const overlay = canClick ? ( const overlay = canClick ? (
// Not sure what this button does.
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<button <button
type="button"
className={overlayClassName} className={overlayClassName}
onClick={this.handleClick} onClick={this.handleClick}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
@ -135,6 +139,7 @@ export class Image extends React.Component<Props> {
/> />
) : null; ) : null;
/* eslint-disable no-nested-ternary */
return ( return (
<div <div
className={classNames( className={classNames(
@ -210,7 +215,8 @@ export class Image extends React.Component<Props> {
{overlay} {overlay}
{closeButton ? ( {closeButton ? (
<button <button
onClick={(e: React.MouseEvent<{}>) => { type="button"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -220,9 +226,11 @@ export class Image extends React.Component<Props> {
}} }}
className="module-image__close-button" className="module-image__close-button"
title={i18n('remove-attachment')} title={i18n('remove-attachment')}
aria-label={i18n('remove-attachment')}
/> />
) : null} ) : null}
</div> </div>
); );
/* eslint-enable no-nested-ternary */
} }
} }

View file

@ -13,12 +13,10 @@ import {
MIMEType, MIMEType,
VIDEO_MP4, VIDEO_MP4,
} from '../../types/MIME'; } from '../../types/MIME';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { pngUrl, squareStickerUrl } from '../../storybook/Fixtures'; import { pngUrl, squareStickerUrl } from '../../storybook/Fixtures';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/ImageGrid', module); const story = storiesOf('Components/Conversation/ImageGrid', module);

View file

@ -30,10 +30,7 @@ export interface Props {
onClick?: (attachment: AttachmentType) => void; onClick?: (attachment: AttachmentType) => void;
} }
export class ImageGrid extends React.Component<Props> { export const ImageGrid = ({
// tslint:disable-next-line max-func-body-length */
public render() {
const {
attachments, attachments,
bottomOverlay, bottomOverlay,
i18n, i18n,
@ -44,12 +41,11 @@ export class ImageGrid extends React.Component<Props> {
tabIndex, tabIndex,
withContentAbove, withContentAbove,
withContentBelow, withContentBelow,
} = this.props; }: Props): JSX.Element | null => {
const curveTopLeft = !withContentAbove;
const curveTopLeft = !Boolean(withContentAbove);
const curveTopRight = curveTopLeft; const curveTopRight = curveTopLeft;
const curveBottom = !Boolean(withContentBelow); const curveBottom = !withContentBelow;
const curveBottomLeft = curveBottom; const curveBottomLeft = curveBottom;
const curveBottomRight = curveBottom; const curveBottomRight = curveBottom;
@ -347,5 +343,4 @@ export class ImageGrid extends React.Component<Props> {
</div> </div>
</div> </div>
); );
} };
}

View file

@ -10,7 +10,7 @@ export type PropsType = {
export class InlineNotificationWrapper extends React.Component<PropsType> { export class InlineNotificationWrapper extends React.Component<PropsType> {
public focusRef: React.RefObject<HTMLDivElement> = React.createRef(); public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
public setFocus = () => { public setFocus = (): void => {
const container = this.focusRef.current; const container = this.focusRef.current;
if (container && !container.contains(document.activeElement)) { if (container && !container.contains(document.activeElement)) {
@ -18,14 +18,15 @@ export class InlineNotificationWrapper extends React.Component<PropsType> {
} }
}; };
public handleFocus = () => { public handleFocus = (): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
if (window.getInteractionMode() === 'keyboard') { if (window.getInteractionMode() === 'keyboard') {
this.setSelected(); this.setSelected();
} }
}; };
public setSelected = () => { public setSelected = (): void => {
const { id, conversationId, selectMessage } = this.props; const { id, conversationId, selectMessage } = this.props;
if (selectMessage) { if (selectMessage) {
@ -33,25 +34,28 @@ export class InlineNotificationWrapper extends React.Component<PropsType> {
} }
}; };
public componentDidMount() { public componentDidMount(): void {
const { isSelected } = this.props; const { isSelected } = this.props;
if (isSelected) { if (isSelected) {
this.setFocus(); this.setFocus();
} }
} }
public componentDidUpdate(prevProps: PropsType) { public componentDidUpdate(prevProps: PropsType): void {
if (!prevProps.isSelected && this.props.isSelected) { const { isSelected } = this.props;
if (!prevProps.isSelected && isSelected) {
this.setFocus(); this.setFocus();
} }
} }
public render() { public render(): JSX.Element {
const { children } = this.props; const { children } = this.props;
return ( return (
<div <div
className="module-inline-notification-wrapper" className="module-inline-notification-wrapper"
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0} tabIndex={0}
ref={this.focusRef} ref={this.focusRef}
onFocus={this.handleFocus} onFocus={this.handleFocus}

View file

@ -4,11 +4,9 @@ import { number } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { LastSeenIndicator, Props } from './LastSeenIndicator'; import { LastSeenIndicator, Props } from './LastSeenIndicator';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/LastSeenIndicator', module); const story = storiesOf('Components/Conversation/LastSeenIndicator', module);

View file

@ -7,10 +7,7 @@ export type Props = {
i18n: LocalizerType; i18n: LocalizerType;
}; };
export class LastSeenIndicator extends React.Component<Props> { export const LastSeenIndicator = ({ count, i18n }: Props): JSX.Element => {
public render() {
const { count, i18n } = this.props;
const message = const message =
count === 1 count === 1
? i18n('unreadMessage') ? i18n('unreadMessage')
@ -22,5 +19,4 @@ export class LastSeenIndicator extends React.Component<Props> {
<div className="module-last-seen-indicator__text">{message}</div> <div className="module-last-seen-indicator__text">{message}</div>
</div> </div>
); );
} };
}

View file

@ -20,17 +20,21 @@ export class Linkify extends React.Component<Props> {
renderNonLink: ({ text }) => text, renderNonLink: ({ text }) => text,
}; };
public render() { public render():
| JSX.Element
| string
| null
| Array<JSX.Element | string | null> {
const { text, renderNonLink } = this.props; const { text, renderNonLink } = this.props;
const matchData = linkify.match(text) || []; const matchData = linkify.match(text) || [];
const results: Array<any> = []; const results: Array<JSX.Element | string> = [];
let last = 0; let last = 0;
let count = 1; let count = 1;
// We have to do this, because renderNonLink is not required in our Props object, // We have to do this, because renderNonLink is not required in our Props object,
// but it is always provided via defaultProps. // but it is always provided via defaultProps.
if (!renderNonLink) { if (!renderNonLink) {
return; return null;
} }
if (matchData.length === 0) { if (matchData.length === 0) {
@ -46,18 +50,20 @@ export class Linkify extends React.Component<Props> {
}) => { }) => {
if (last < match.index) { if (last < match.index) {
const textWithNoLink = text.slice(last, match.index); const textWithNoLink = text.slice(last, match.index);
results.push(renderNonLink({ text: textWithNoLink, key: count++ })); count += 1;
results.push(renderNonLink({ text: textWithNoLink, key: count }));
} }
const { url, text: originalText } = match; const { url, text: originalText } = match;
count += 1;
if (SUPPORTED_PROTOCOLS.test(url) && !isLinkSneaky(url)) { if (SUPPORTED_PROTOCOLS.test(url) && !isLinkSneaky(url)) {
results.push( results.push(
<a key={count++} href={url}> <a key={count} href={url}>
{originalText} {originalText}
</a> </a>
); );
} else { } else {
results.push(renderNonLink({ text: originalText, key: count++ })); results.push(renderNonLink({ text: originalText, key: count }));
} }
last = match.lastIndex; last = match.lastIndex;
@ -65,7 +71,8 @@ export class Linkify extends React.Component<Props> {
); );
if (last < text.length) { if (last < text.length) {
results.push(renderNonLink({ text: text.slice(last), key: count++ })); count += 1;
results.push(renderNonLink({ text: text.slice(last), key: count }));
} }
return results; return results;

View file

@ -15,12 +15,10 @@ import {
MIMEType, MIMEType,
VIDEO_MP4, VIDEO_MP4,
} from '../../types/MIME'; } from '../../types/MIME';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { pngUrl } from '../../storybook/Fixtures'; import { pngUrl } from '../../storybook/Fixtures';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/Message', module); const story = storiesOf('Components/Conversation/Message', module);
@ -75,7 +73,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
previews: overrideProps.previews || [], previews: overrideProps.previews || [],
reactions: overrideProps.reactions, reactions: overrideProps.reactions,
reactToMessage: action('reactToMessage'), reactToMessage: action('reactToMessage'),
renderEmojiPicker: renderEmojiPicker, renderEmojiPicker,
replyToMessage: action('replyToMessage'), replyToMessage: action('replyToMessage'),
retrySend: action('retrySend'), retrySend: action('retrySend'),
scrollToQuotedMessage: action('scrollToQuotedMessage'), scrollToQuotedMessage: action('scrollToQuotedMessage'),
@ -195,7 +193,6 @@ story.add('Older', () => {
return renderBothDirections(props); return renderBothDirections(props);
}); });
// tslint:disable-next-line:max-func-body-length
story.add('Reactions', () => { story.add('Reactions', () => {
const props = createProps({ const props = createProps({
text: 'Hello there from a pal!', text: 'Hello there from a pal!',

View file

@ -3,6 +3,7 @@ import ReactDOM, { createPortal } from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import Measure from 'react-measure'; import Measure from 'react-measure';
import { drop, groupBy, orderBy, take } from 'lodash'; import { drop, groupBy, orderBy, take } from 'lodash';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
import { Manager, Popper, Reference } from 'react-popper'; import { Manager, Popper, Reference } from 'react-popper';
import moment, { Moment } from 'moment'; import moment, { Moment } from 'moment';
@ -24,6 +25,7 @@ import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker';
import { Emoji } from '../emoji/Emoji'; import { Emoji } from '../emoji/Emoji';
import { import {
AttachmentType,
canDisplayImage, canDisplayImage,
getExtensionForDisplay, getExtensionForDisplay,
getGridDimensions, getGridDimensions,
@ -34,8 +36,7 @@ import {
isImage, isImage,
isImageAttachment, isImageAttachment,
isVideo, isVideo,
} from '../../../ts/types/Attachment'; } from '../../types/Attachment';
import { AttachmentType } from '../../types/Attachment';
import { ContactType } from '../../types/Contact'; import { ContactType } from '../../types/Contact';
import { getIncrement } from '../../util/timer'; import { getIncrement } from '../../util/timer';
@ -43,7 +44,6 @@ import { isFileDangerous } from '../../util/isFileDangerous';
import { BodyRangesType, LocalizerType } from '../../types/Util'; import { BodyRangesType, LocalizerType } from '../../types/Util';
import { ColorType } from '../../types/Colors'; import { ColorType } from '../../types/Colors';
import { createRefMerger } from '../_util'; import { createRefMerger } from '../_util';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
interface Trigger { interface Trigger {
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void; handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
@ -209,18 +209,24 @@ const EXPIRED_DELAY = 600;
export class Message extends React.PureComponent<Props, State> { export class Message extends React.PureComponent<Props, State> {
public menuTriggerRef: Trigger | undefined; public menuTriggerRef: Trigger | undefined;
public audioRef: React.RefObject<HTMLAudioElement> = React.createRef(); public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();
public focusRef: React.RefObject<HTMLDivElement> = React.createRef(); public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
public reactionsContainerRef: React.RefObject< public reactionsContainerRef: React.RefObject<
HTMLDivElement HTMLDivElement
> = React.createRef(); > = React.createRef();
public reactionsContainerRefMerger = createRefMerger(); public reactionsContainerRefMerger = createRefMerger();
public wideMl: MediaQueryList; public wideMl: MediaQueryList;
public expirationCheckInterval: any; public expirationCheckInterval: NodeJS.Timeout | undefined;
public expiredTimeout: any;
public selectedTimeout: any; public expiredTimeout: NodeJS.Timeout | undefined;
public selectedTimeout: NodeJS.Timeout | undefined;
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
@ -268,24 +274,23 @@ export class Message extends React.PureComponent<Props, State> {
return state; return state;
} }
public handleWideMlChange = (event: MediaQueryListEvent) => { public handleWideMlChange = (event: MediaQueryListEvent): void => {
this.setState({ isWide: event.matches }); this.setState({ isWide: event.matches });
}; };
public captureMenuTrigger = (triggerRef: Trigger) => { public captureMenuTrigger = (triggerRef: Trigger): void => {
this.menuTriggerRef = triggerRef; this.menuTriggerRef = triggerRef;
}; };
public showMenu = (event: React.MouseEvent<HTMLDivElement>) => { public showMenu = (event: React.MouseEvent<HTMLDivElement>): void => {
if (this.menuTriggerRef) { if (this.menuTriggerRef) {
this.menuTriggerRef.handleContextClick(event); this.menuTriggerRef.handleContextClick(event);
} }
}; };
public handleImageError = () => { public handleImageError = (): void => {
const { id } = this.props; const { id } = this.props;
// tslint:disable-next-line no-console window.log.info(
console.log(
`Message ${id}: Image failed to load; failing over to placeholder` `Message ${id}: Image failed to load; failing over to placeholder`
); );
this.setState({ this.setState({
@ -293,7 +298,7 @@ export class Message extends React.PureComponent<Props, State> {
}); });
}; };
public handleFocus = () => { public handleFocus = (): void => {
const { interactionMode } = this.props; const { interactionMode } = this.props;
if (interactionMode === 'keyboard') { if (interactionMode === 'keyboard') {
@ -301,7 +306,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
}; };
public setSelected = () => { public setSelected = (): void => {
const { id, conversationId, selectMessage } = this.props; const { id, conversationId, selectMessage } = this.props;
if (selectMessage) { if (selectMessage) {
@ -309,7 +314,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
}; };
public setFocus = () => { public setFocus = (): void => {
const container = this.focusRef.current; const container = this.focusRef.current;
if (container && !container.contains(document.activeElement)) { if (container && !container.contains(document.activeElement)) {
@ -317,7 +322,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
}; };
public componentDidMount() { public componentDidMount(): void {
this.startSelectedTimer(); this.startSelectedTimer();
const { isSelected } = this.props; const { isSelected } = this.props;
@ -340,7 +345,7 @@ export class Message extends React.PureComponent<Props, State> {
}, checkFrequency); }, checkFrequency);
} }
public componentWillUnmount() { public componentWillUnmount(): void {
if (this.selectedTimeout) { if (this.selectedTimeout) {
clearInterval(this.selectedTimeout); clearInterval(this.selectedTimeout);
} }
@ -356,18 +361,20 @@ export class Message extends React.PureComponent<Props, State> {
this.wideMl.removeEventListener('change', this.handleWideMlChange); this.wideMl.removeEventListener('change', this.handleWideMlChange);
} }
public componentDidUpdate(prevProps: Props) { public componentDidUpdate(prevProps: Props): void {
const { isSelected } = this.props;
this.startSelectedTimer(); this.startSelectedTimer();
if (!prevProps.isSelected && this.props.isSelected) { if (!prevProps.isSelected && isSelected) {
this.setFocus(); this.setFocus();
} }
this.checkExpired(); this.checkExpired();
} }
public startSelectedTimer() { public startSelectedTimer(): void {
const { interactionMode } = this.props; const { clearSelectedMessage, interactionMode } = this.props;
const { isSelected } = this.state; const { isSelected } = this.state;
if (interactionMode === 'keyboard' || !isSelected) { if (interactionMode === 'keyboard' || !isSelected) {
@ -378,12 +385,12 @@ export class Message extends React.PureComponent<Props, State> {
this.selectedTimeout = setTimeout(() => { this.selectedTimeout = setTimeout(() => {
this.selectedTimeout = undefined; this.selectedTimeout = undefined;
this.setState({ isSelected: false }); this.setState({ isSelected: false });
this.props.clearSelectedMessage(); clearSelectedMessage();
}, SELECTED_TIMEOUT); }, SELECTED_TIMEOUT);
} }
} }
public checkExpired() { public checkExpired(): void {
const now = Date.now(); const now = Date.now();
const { isExpired, expirationTimestamp, expirationLength } = this.props; const { isExpired, expirationTimestamp, expirationLength } = this.props;
@ -408,7 +415,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
} }
public renderTimestamp() { public renderTimestamp(): JSX.Element {
const { const {
direction, direction,
i18n, i18n,
@ -442,6 +449,7 @@ export class Message extends React.PureComponent<Props, State> {
i18n('sendFailed') i18n('sendFailed')
) : ( ) : (
<button <button
type="button"
className="module-message__metadata__tapable" className="module-message__metadata__tapable"
onClick={(event: React.MouseEvent) => { onClick={(event: React.MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
@ -463,7 +471,7 @@ export class Message extends React.PureComponent<Props, State> {
<Timestamp <Timestamp
i18n={i18n} i18n={i18n}
timestamp={timestamp} timestamp={timestamp}
extended={true} extended
direction={metadataDirection} direction={metadataDirection}
withImageNoCaption={withImageNoCaption} withImageNoCaption={withImageNoCaption}
withSticker={isSticker} withSticker={isSticker}
@ -473,8 +481,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
// tslint:disable-next-line cyclomatic-complexity public renderMetadata(): JSX.Element | null {
public renderMetadata() {
const { const {
collapseMetadata, collapseMetadata,
direction, direction,
@ -548,7 +555,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderAuthor() { public renderAuthor(): JSX.Element | null {
const { const {
authorTitle, authorTitle,
authorName, authorName,
@ -564,7 +571,7 @@ export class Message extends React.PureComponent<Props, State> {
} = this.props; } = this.props;
if (collapseMetadata) { if (collapseMetadata) {
return; return null;
} }
if ( if (
@ -597,8 +604,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
// tslint:disable-next-line max-func-body-length cyclomatic-complexity public renderAttachment(): JSX.Element | null {
public renderAttachment() {
const { const {
attachments, attachments,
collapseMetadata, collapseMetadata,
@ -667,11 +673,12 @@ export class Message extends React.PureComponent<Props, State> {
/> />
</div> </div>
); );
} else if (!firstAttachment.pending && isAudio(attachments)) { }
if (!firstAttachment.pending && isAudio(attachments)) {
return ( return (
<audio <audio
ref={this.audioRef} ref={this.audioRef}
controls={true} controls
className={classNames( className={classNames(
'module-message__audio-attachment', 'module-message__audio-attachment',
withContentBelow withContentBelow
@ -686,13 +693,14 @@ export class Message extends React.PureComponent<Props, State> {
<source src={firstAttachment.url} /> <source src={firstAttachment.url} />
</audio> </audio>
); );
} else { }
const { pending, fileName, fileSize, contentType } = firstAttachment; const { pending, fileName, fileSize, contentType } = firstAttachment;
const extension = getExtensionForDisplay({ contentType, fileName }); const extension = getExtensionForDisplay({ contentType, fileName });
const isDangerous = isFileDangerous(fileName || ''); const isDangerous = isFileDangerous(fileName || '');
return ( return (
<button <button
type="button"
className={classNames( className={classNames(
'module-message__generic-attachment', 'module-message__generic-attachment',
withContentBelow withContentBelow
@ -759,10 +767,8 @@ export class Message extends React.PureComponent<Props, State> {
</button> </button>
); );
} }
}
// tslint:disable-next-line cyclomatic-complexity max-func-body-length public renderPreview(): JSX.Element | null {
public renderPreview() {
const { const {
attachments, attachments,
conversationType, conversationType,
@ -809,6 +815,7 @@ export class Message extends React.PureComponent<Props, State> {
return ( return (
<button <button
type="button"
className={classNames( className={classNames(
'module-message__link-preview', 'module-message__link-preview',
`module-message__link-preview--${direction}`, `module-message__link-preview--${direction}`,
@ -835,7 +842,7 @@ export class Message extends React.PureComponent<Props, State> {
<ImageGrid <ImageGrid
attachments={[first.image]} attachments={[first.image]}
withContentAbove={withContentAbove} withContentAbove={withContentAbove}
withContentBelow={true} withContentBelow
onError={this.handleImageError} onError={this.handleImageError}
i18n={i18n} i18n={i18n}
/> />
@ -852,9 +859,9 @@ export class Message extends React.PureComponent<Props, State> {
<div className="module-message__link-preview__icon_container"> <div className="module-message__link-preview__icon_container">
<Image <Image
smallCurveTopLeft={!withContentAbove} smallCurveTopLeft={!withContentAbove}
noBorder={true} noBorder
noBackground={true} noBackground
softCorners={true} softCorners
alt={i18n('previewThumbnail', [first.domain])} alt={i18n('previewThumbnail', [first.domain])}
height={72} height={72}
width={72} width={72}
@ -900,7 +907,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderQuote() { public renderQuote(): JSX.Element | null {
const { const {
conversationType, conversationType,
authorColor, authorColor,
@ -952,7 +959,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderEmbeddedContact() { public renderEmbeddedContact(): JSX.Element | null {
const { const {
collapseMetadata, collapseMetadata,
contact, contact,
@ -989,7 +996,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderSendMessageButton() { public renderSendMessageButton(): JSX.Element | null {
const { contact, openConversation, i18n } = this.props; const { contact, openConversation, i18n } = this.props;
if (!contact || !contact.signalAccount) { if (!contact || !contact.signalAccount) {
return null; return null;
@ -997,6 +1004,7 @@ export class Message extends React.PureComponent<Props, State> {
return ( return (
<button <button
type="button"
onClick={() => { onClick={() => {
if (contact.signalAccount) { if (contact.signalAccount) {
openConversation(contact.signalAccount); openConversation(contact.signalAccount);
@ -1009,7 +1017,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderAvatar() { public renderAvatar(): JSX.Element | undefined {
const { const {
authorAvatarPath, authorAvatarPath,
authorName, authorName,
@ -1031,6 +1039,7 @@ export class Message extends React.PureComponent<Props, State> {
return; return;
} }
// eslint-disable-next-line consistent-return
return ( return (
<div className="module-message__author-avatar"> <div className="module-message__author-avatar">
<Avatar <Avatar
@ -1048,7 +1057,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderText() { public renderText(): JSX.Element | null {
const { const {
bodyRanges, bodyRanges,
deletedForEveryone, deletedForEveryone,
@ -1060,6 +1069,7 @@ export class Message extends React.PureComponent<Props, State> {
textPending, textPending,
} = this.props; } = this.props;
// eslint-disable-next-line no-nested-ternary
const contents = deletedForEveryone const contents = deletedForEveryone
? i18n('message--deletedForEveryone') ? i18n('message--deletedForEveryone')
: direction === 'incoming' && status === 'error' : direction === 'incoming' && status === 'error'
@ -1093,7 +1103,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderError(isCorrectSide: boolean) { public renderError(isCorrectSide: boolean): JSX.Element | null {
const { status, direction } = this.props; const { status, direction } = this.props;
if (!isCorrectSide || (status !== 'error' && status !== 'partial-sent')) { if (!isCorrectSide || (status !== 'error' && status !== 'partial-sent')) {
@ -1112,10 +1122,12 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderMenu(isCorrectSide: boolean, triggerId: string) { public renderMenu(
isCorrectSide: boolean,
triggerId: string
): JSX.Element | null {
const { const {
attachments, attachments,
// tslint:disable-next-line max-func-body-length
canReply, canReply,
direction, direction,
disableMenu, disableMenu,
@ -1123,8 +1135,10 @@ export class Message extends React.PureComponent<Props, State> {
id, id,
isSticker, isSticker,
isTapToView, isTapToView,
reactToMessage,
renderEmojiPicker, renderEmojiPicker,
replyToMessage, replyToMessage,
selectedReaction,
} = this.props; } = this.props;
if (!isCorrectSide || disableMenu) { if (!isCorrectSide || disableMenu) {
@ -1142,10 +1156,13 @@ export class Message extends React.PureComponent<Props, State> {
!isTapToView && !isTapToView &&
firstAttachment && firstAttachment &&
!firstAttachment.pending ? ( !firstAttachment.pending ? (
// This a menu meant for mouse use only
// eslint-disable-next-line max-len
// eslint-disable-next-line jsx-a11y/interactive-supports-focus, jsx-a11y/click-events-have-key-events
<div <div
onClick={this.openGenericAttachment} onClick={this.openGenericAttachment}
// This a menu meant for mouse use only
role="button" role="button"
aria-label={i18n('downloadAttachment')}
className={classNames( className={classNames(
'module-message__buttons__download', 'module-message__buttons__download',
`module-message__buttons__download--${direction}` `module-message__buttons__download--${direction}`
@ -1161,6 +1178,9 @@ export class Message extends React.PureComponent<Props, State> {
const maybePopperRef = isWide ? popperRef : undefined; const maybePopperRef = isWide ? popperRef : undefined;
return ( return (
// This a menu meant for mouse use only
// eslint-disable-next-line max-len
// eslint-disable-next-line jsx-a11y/interactive-supports-focus, jsx-a11y/click-events-have-key-events
<div <div
ref={maybePopperRef} ref={maybePopperRef}
onClick={(event: React.MouseEvent) => { onClick={(event: React.MouseEvent) => {
@ -1171,6 +1191,7 @@ export class Message extends React.PureComponent<Props, State> {
}} }}
role="button" role="button"
className="module-message__buttons__react" className="module-message__buttons__react"
aria-label={i18n('reactToMessage')}
/> />
); );
}} }}
@ -1178,6 +1199,9 @@ export class Message extends React.PureComponent<Props, State> {
); );
const replyButton = ( const replyButton = (
// This a menu meant for mouse use only
// eslint-disable-next-line max-len
// eslint-disable-next-line jsx-a11y/interactive-supports-focus, jsx-a11y/click-events-have-key-events
<div <div
onClick={(event: React.MouseEvent) => { onClick={(event: React.MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
@ -1187,6 +1211,7 @@ export class Message extends React.PureComponent<Props, State> {
}} }}
// This a menu meant for mouse use only // This a menu meant for mouse use only
role="button" role="button"
aria-label={i18n('replyToMessage')}
className={classNames( className={classNames(
'module-message__buttons__reply', 'module-message__buttons__reply',
`module-message__buttons__download--${direction}` `module-message__buttons__download--${direction}`
@ -1194,6 +1219,9 @@ export class Message extends React.PureComponent<Props, State> {
/> />
); );
// This a menu meant for mouse use only
/* eslint-disable jsx-a11y/interactive-supports-focus */
/* eslint-disable jsx-a11y/click-events-have-key-events */
const menuButton = ( const menuButton = (
<Reference> <Reference>
{({ ref: popperRef }) => { {({ ref: popperRef }) => {
@ -1205,13 +1233,14 @@ export class Message extends React.PureComponent<Props, State> {
return ( return (
<ContextMenuTrigger <ContextMenuTrigger
id={triggerId} id={triggerId}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref={this.captureMenuTrigger as any} ref={this.captureMenuTrigger as any}
> >
<div <div
// This a menu meant for mouse use only
ref={maybePopperRef} ref={maybePopperRef}
role="button" role="button"
onClick={this.showMenu} onClick={this.showMenu}
aria-label={i18n('messageContextMenuButton')}
className={classNames( className={classNames(
'module-message__buttons__menu', 'module-message__buttons__menu',
`module-message__buttons__download--${direction}` `module-message__buttons__download--${direction}`
@ -1222,6 +1251,8 @@ export class Message extends React.PureComponent<Props, State> {
}} }}
</Reference> </Reference>
); );
/* eslint-enable jsx-a11y/interactive-supports-focus */
/* eslint-enable jsx-a11y/click-events-have-key-events */
return ( return (
<Manager> <Manager>
@ -1238,19 +1269,20 @@ export class Message extends React.PureComponent<Props, State> {
</div> </div>
{reactionPickerRoot && {reactionPickerRoot &&
createPortal( createPortal(
// eslint-disable-next-line consistent-return
<Popper placement="top"> <Popper placement="top">
{({ ref, style }) => ( {({ ref, style }) => (
<ReactionPicker <ReactionPicker
i18n={i18n} i18n={i18n}
ref={ref} ref={ref}
style={style} style={style}
selected={this.props.selectedReaction} selected={selectedReaction}
onClose={this.toggleReactionPicker} onClose={this.toggleReactionPicker}
onPick={emoji => { onPick={emoji => {
this.toggleReactionPicker(true); this.toggleReactionPicker(true);
this.props.reactToMessage(id, { reactToMessage(id, {
emoji, emoji,
remove: emoji === this.props.selectedReaction, remove: emoji === selectedReaction,
}); });
}} }}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
@ -1263,8 +1295,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
// tslint:disable-next-line max-func-body-length public renderContextMenu(triggerId: string): JSX.Element {
public renderContextMenu(triggerId: string) {
const { const {
attachments, attachments,
canReply, canReply,
@ -1396,7 +1427,7 @@ export class Message extends React.PureComponent<Props, State> {
const first = previews[0]; const first = previews[0];
if (!first || !first.image) { if (!first || !first.image) {
return; return undefined;
} }
const { width } = first.image; const { width } = first.image;
@ -1414,9 +1445,11 @@ export class Message extends React.PureComponent<Props, State> {
} }
} }
return; return undefined;
} }
// Messy return here.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public isShowingImage() { public isShowingImage() {
const { isTapToView, attachments, previews } = this.props; const { isTapToView, attachments, previews } = this.props;
const { imageBroken } = this.state; const { imageBroken } = this.state;
@ -1449,7 +1482,7 @@ export class Message extends React.PureComponent<Props, State> {
return false; return false;
} }
public isAttachmentPending() { public isAttachmentPending(): boolean {
const { attachments } = this.props; const { attachments } = this.props;
if (!attachments || attachments.length < 1) { if (!attachments || attachments.length < 1) {
@ -1461,7 +1494,7 @@ export class Message extends React.PureComponent<Props, State> {
return Boolean(first.pending); return Boolean(first.pending);
} }
public renderTapToViewIcon() { public renderTapToViewIcon(): JSX.Element {
const { direction, isTapToViewExpired } = this.props; const { direction, isTapToViewExpired } = this.props;
const isDownloadPending = this.isAttachmentPending(); const isDownloadPending = this.isAttachmentPending();
@ -1482,7 +1515,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderTapToViewText() { public renderTapToViewText(): string | undefined {
const { const {
attachments, attachments,
direction, direction,
@ -1505,6 +1538,7 @@ export class Message extends React.PureComponent<Props, State> {
return; return;
} }
// eslint-disable-next-line consistent-return, no-nested-ternary
return isTapToViewError return isTapToViewError
? i18n('incomingError') ? i18n('incomingError')
: direction === 'outgoing' : direction === 'outgoing'
@ -1512,7 +1546,7 @@ export class Message extends React.PureComponent<Props, State> {
: incomingString; : incomingString;
} }
public renderTapToView() { public renderTapToView(): JSX.Element {
const { const {
collapseMetadata, collapseMetadata,
conversationType, conversationType,
@ -1558,7 +1592,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public toggleReactionViewer = (onlyRemove = false) => { public toggleReactionViewer = (onlyRemove = false): void => {
this.setState(({ reactionViewerRoot }) => { this.setState(({ reactionViewerRoot }) => {
if (reactionViewerRoot) { if (reactionViewerRoot) {
document.body.removeChild(reactionViewerRoot); document.body.removeChild(reactionViewerRoot);
@ -1589,7 +1623,7 @@ export class Message extends React.PureComponent<Props, State> {
}); });
}; };
public toggleReactionPicker = (onlyRemove = false) => { public toggleReactionPicker = (onlyRemove = false): void => {
this.setState(({ reactionPickerRoot }) => { this.setState(({ reactionPickerRoot }) => {
if (reactionPickerRoot) { if (reactionPickerRoot) {
document.body.removeChild(reactionPickerRoot); document.body.removeChild(reactionPickerRoot);
@ -1620,7 +1654,7 @@ export class Message extends React.PureComponent<Props, State> {
}); });
}; };
public handleClickOutsideReactionViewer = (e: MouseEvent) => { public handleClickOutsideReactionViewer = (e: MouseEvent): void => {
const { reactionViewerRoot } = this.state; const { reactionViewerRoot } = this.state;
const { current: reactionsContainer } = this.reactionsContainerRef; const { current: reactionsContainer } = this.reactionsContainerRef;
if (reactionViewerRoot && reactionsContainer) { if (reactionViewerRoot && reactionsContainer) {
@ -1633,7 +1667,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
}; };
public handleClickOutsideReactionPicker = (e: MouseEvent) => { public handleClickOutsideReactionPicker = (e: MouseEvent): void => {
const { reactionPickerRoot } = this.state; const { reactionPickerRoot } = this.state;
if (reactionPickerRoot) { if (reactionPickerRoot) {
if (!reactionPickerRoot.contains(e.target as HTMLElement)) { if (!reactionPickerRoot.contains(e.target as HTMLElement)) {
@ -1642,8 +1676,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
}; };
// tslint:disable-next-line max-func-body-length public renderReactions(outgoing: boolean): JSX.Element | null {
public renderReactions(outgoing: boolean) {
const { reactions, i18n } = this.props; const { reactions, i18n } = this.props;
if (!reactions || (reactions && reactions.length === 0)) { if (!reactions || (reactions && reactions.length === 0)) {
@ -1726,6 +1759,8 @@ export class Message extends React.PureComponent<Props, State> {
return ( return (
<button <button
type="button"
// eslint-disable-next-line react/no-array-index-key
key={`${re.emoji}-${i}`} key={`${re.emoji}-${i}`}
className={classNames( className={classNames(
'module-message__reactions__reaction', 'module-message__reactions__reaction',
@ -1764,7 +1799,7 @@ export class Message extends React.PureComponent<Props, State> {
+{maybeNotRenderedTotal} +{maybeNotRenderedTotal}
</span> </span>
) : ( ) : (
<React.Fragment> <>
<Emoji size={16} emoji={re.emoji} /> <Emoji size={16} emoji={re.emoji} />
{re.count > 1 ? ( {re.count > 1 ? (
<span <span
@ -1778,7 +1813,7 @@ export class Message extends React.PureComponent<Props, State> {
{re.count} {re.count}
</span> </span>
) : null} ) : null}
</React.Fragment> </>
)} )}
</button> </button>
); );
@ -1808,7 +1843,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
public renderContents() { public renderContents(): JSX.Element | null {
const { isTapToView, deletedForEveryone } = this.props; const { isTapToView, deletedForEveryone } = this.props;
if (deletedForEveryone) { if (deletedForEveryone) {
@ -1837,10 +1872,9 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
public handleOpen = ( public handleOpen = (
event: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent event: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent
) => { ): void => {
const { const {
attachments, attachments,
contact, contact,
@ -1923,10 +1957,8 @@ export class Message extends React.PureComponent<Props, State> {
event.stopPropagation(); event.stopPropagation();
if (this.audioRef.current.paused) { if (this.audioRef.current.paused) {
// tslint:disable-next-line no-floating-promises
this.audioRef.current.play(); this.audioRef.current.play();
} else { } else {
// tslint:disable-next-line no-floating-promises
this.audioRef.current.pause(); this.audioRef.current.pause();
} }
} }
@ -1946,7 +1978,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
}; };
public openGenericAttachment = (event?: React.MouseEvent) => { public openGenericAttachment = (event?: React.MouseEvent): void => {
const { attachments, downloadAttachment, timestamp } = this.props; const { attachments, downloadAttachment, timestamp } = this.props;
if (event) { if (event) {
@ -1969,7 +2001,7 @@ export class Message extends React.PureComponent<Props, State> {
}); });
}; };
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => { public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
// Do not allow reactions to error messages // Do not allow reactions to error messages
const { canReply } = this.props; const { canReply } = this.props;
@ -1989,7 +2021,7 @@ export class Message extends React.PureComponent<Props, State> {
this.handleOpen(event); this.handleOpen(event);
}; };
public handleClick = (event: React.MouseEvent) => { public handleClick = (event: React.MouseEvent): void => {
// We don't want clicks on body text to result in the 'default action' for the message // We don't want clicks on body text to result in the 'default action' for the message
const { text } = this.props; const { text } = this.props;
if (text && text.length > 0) { if (text && text.length > 0) {
@ -2008,8 +2040,7 @@ export class Message extends React.PureComponent<Props, State> {
this.handleOpen(event); this.handleOpen(event);
}; };
// tslint:disable-next-line: cyclomatic-complexity public renderContainer(): JSX.Element {
public renderContainer() {
const { const {
authorColor, authorColor,
deletedForEveryone, deletedForEveryone,
@ -2061,7 +2092,7 @@ export class Message extends React.PureComponent<Props, State> {
return ( return (
<Measure <Measure
bounds={true} bounds
onResize={({ bounds = { width: 0 } }) => { onResize={({ bounds = { width: 0 } }) => {
this.setState({ containerWidth: bounds.width }); this.setState({ containerWidth: bounds.width });
}} }}
@ -2081,8 +2112,7 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
// tslint:disable-next-line cyclomatic-complexity public render(): JSX.Element | null {
public render() {
const { const {
authorPhoneNumber, authorPhoneNumber,
attachments, attachments,

View file

@ -4,11 +4,9 @@ import { boolean, text } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { MessageBody, Props } from './MessageBody'; import { MessageBody, Props } from './MessageBody';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/MessageBody', module); const story = storiesOf('Components/Conversation/MessageBody', module);

View file

@ -94,7 +94,7 @@ export class MessageBody extends React.Component<Props> {
); );
} }
public render() { public render(): JSX.Element {
const { const {
bodyRanges, bodyRanges,
text, text,

View file

@ -6,11 +6,9 @@ import { storiesOf } from '@storybook/react';
import { Props as MessageProps } from './Message'; import { Props as MessageProps } from './Message';
import { MessageDetail, Props } from './MessageDetail'; import { MessageDetail, Props } from './MessageDetail';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/MessageDetail', module); const story = storiesOf('Components/Conversation/MessageDetail', module);
@ -147,6 +145,7 @@ story.add('Not Delivered', () => {
text: 'A message to Max', text: 'A message to Max',
}, },
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props.receivedAt = undefined as any; props.receivedAt = undefined as any;
return <MessageDetail {...props} />; return <MessageDetail {...props} />;

View file

@ -37,10 +37,14 @@ export interface Props {
i18n: LocalizerType; i18n: LocalizerType;
} }
const _keyForError = (error: Error): string => {
return `${error.name}-${error.message}`;
};
export class MessageDetail extends React.Component<Props> { export class MessageDetail extends React.Component<Props> {
private readonly focusRef = React.createRef<HTMLDivElement>(); private readonly focusRef = React.createRef<HTMLDivElement>();
public componentDidMount() { public componentDidMount(): void {
// When this component is created, it's initially not part of the DOM, and then it's // When this component is created, it's initially not part of the DOM, and then it's
// added off-screen and animated in. This ensures that the focus takes. // added off-screen and animated in. This ensures that the focus takes.
setTimeout(() => { setTimeout(() => {
@ -50,7 +54,7 @@ export class MessageDetail extends React.Component<Props> {
}); });
} }
public renderAvatar(contact: Contact) { public renderAvatar(contact: Contact): JSX.Element {
const { i18n } = this.props; const { i18n } = this.props;
const { const {
avatarPath, avatarPath,
@ -76,12 +80,13 @@ export class MessageDetail extends React.Component<Props> {
); );
} }
public renderDeleteButton() { public renderDeleteButton(): JSX.Element {
const { i18n, message } = this.props; const { i18n, message } = this.props;
return ( return (
<div className="module-message-detail__delete-button-container"> <div className="module-message-detail__delete-button-container">
<button <button
type="button"
onClick={() => { onClick={() => {
message.deleteMessage(message.id); message.deleteMessage(message.id);
}} }}
@ -93,19 +98,21 @@ export class MessageDetail extends React.Component<Props> {
); );
} }
public renderContact(contact: Contact) { public renderContact(contact: Contact): JSX.Element {
const { i18n } = this.props; const { i18n } = this.props;
const errors = contact.errors || []; const errors = contact.errors || [];
const errorComponent = contact.isOutgoingKeyError ? ( const errorComponent = contact.isOutgoingKeyError ? (
<div className="module-message-detail__contact__error-buttons"> <div className="module-message-detail__contact__error-buttons">
<button <button
type="button"
className="module-message-detail__contact__show-safety-number" className="module-message-detail__contact__show-safety-number"
onClick={contact.onShowSafetyNumber} onClick={contact.onShowSafetyNumber}
> >
{i18n('showSafetyNumber')} {i18n('showSafetyNumber')}
</button> </button>
<button <button
type="button"
className="module-message-detail__contact__send-anyway" className="module-message-detail__contact__send-anyway"
onClick={contact.onSendAnyway} onClick={contact.onSendAnyway}
> >
@ -138,8 +145,11 @@ export class MessageDetail extends React.Component<Props> {
i18n={i18n} i18n={i18n}
/> />
</div> </div>
{errors.map((error, index) => ( {errors.map(error => (
<div key={index} className="module-message-detail__contact__error"> <div
key={_keyForError(error)}
className="module-message-detail__contact__error"
>
{error.message} {error.message}
</div> </div>
))} ))}
@ -151,7 +161,7 @@ export class MessageDetail extends React.Component<Props> {
); );
} }
public renderContacts() { public renderContacts(): JSX.Element | null {
const { contacts } = this.props; const { contacts } = this.props;
if (!contacts || !contacts.length) { if (!contacts || !contacts.length) {
@ -165,18 +175,19 @@ export class MessageDetail extends React.Component<Props> {
); );
} }
public render() { public render(): JSX.Element {
const { errors, message, receivedAt, sentAt, i18n } = this.props; const { errors, message, receivedAt, sentAt, i18n } = this.props;
return ( return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}> <div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
<div className="module-message-detail__message-container"> <div className="module-message-detail__message-container">
<Message i18n={i18n} {...message} /> <Message i18n={i18n} {...message} />
</div> </div>
<table className="module-message-detail__info"> <table className="module-message-detail__info">
<tbody> <tbody>
{(errors || []).map((error, index) => ( {(errors || []).map(error => (
<tr key={index}> <tr key={_keyForError(error)}>
<td className="module-message-detail__label"> <td className="module-message-detail__label">
{i18n('error')} {i18n('error')}
</td> </td>

View file

@ -2,14 +2,12 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs'; import { text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { import {
MessageRequestActions, MessageRequestActions,
Props as MessageRequestActionsProps, Props as MessageRequestActionsProps,
} from './MessageRequestActions'; } from './MessageRequestActions';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -42,7 +40,7 @@ storiesOf('Components/Conversation/MessageRequestActions', module)
.add('Direct (Blocked)', () => { .add('Direct (Blocked)', () => {
return ( return (
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<MessageRequestActions {...getBaseProps()} isBlocked={true} /> <MessageRequestActions {...getBaseProps()} isBlocked />
</div> </div>
); );
}) })
@ -56,7 +54,7 @@ storiesOf('Components/Conversation/MessageRequestActions', module)
.add('Group (Blocked)', () => { .add('Group (Blocked)', () => {
return ( return (
<div style={{ width: '480px' }}> <div style={{ width: '480px' }}>
<MessageRequestActions {...getBaseProps(true)} isBlocked={true} /> <MessageRequestActions {...getBaseProps(true)} isBlocked />
</div> </div>
); );
}); });

View file

@ -19,7 +19,6 @@ export type Props = {
'i18n' | 'state' | 'onChangeState' 'i18n' | 'state' | 'onChangeState'
>; >;
// tslint:disable-next-line max-func-body-length
export const MessageRequestActions = ({ export const MessageRequestActions = ({
conversationType, conversationType,
firstName, firstName,
@ -34,7 +33,7 @@ export const MessageRequestActions = ({
phoneNumber, phoneNumber,
profileName, profileName,
title, title,
}: Props) => { }: Props): JSX.Element => {
const [mrState, setMrState] = React.useState(MessageRequestState.default); const [mrState, setMrState] = React.useState(MessageRequestState.default);
return ( return (
@ -80,6 +79,7 @@ export const MessageRequestActions = ({
</p> </p>
<div className="module-message-request-actions__buttons"> <div className="module-message-request-actions__buttons">
<button <button
type="button"
onClick={() => { onClick={() => {
setMrState(MessageRequestState.deleting); setMrState(MessageRequestState.deleting);
}} }}
@ -93,6 +93,7 @@ export const MessageRequestActions = ({
</button> </button>
{isBlocked ? ( {isBlocked ? (
<button <button
type="button"
onClick={() => { onClick={() => {
setMrState(MessageRequestState.unblocking); setMrState(MessageRequestState.unblocking);
}} }}
@ -106,6 +107,7 @@ export const MessageRequestActions = ({
</button> </button>
) : ( ) : (
<button <button
type="button"
onClick={() => { onClick={() => {
setMrState(MessageRequestState.blocking); setMrState(MessageRequestState.blocking);
}} }}
@ -120,6 +122,7 @@ export const MessageRequestActions = ({
)} )}
{!isBlocked ? ( {!isBlocked ? (
<button <button
type="button"
onClick={onAccept} onClick={onAccept}
tabIndex={0} tabIndex={0}
className={classNames( className={classNames(

View file

@ -23,7 +23,6 @@ export type Props = {
onChangeState(state: MessageRequestState): unknown; onChangeState(state: MessageRequestState): unknown;
} & Omit<ContactNameProps, 'module' | 'i18n'>; } & Omit<ContactNameProps, 'module' | 'i18n'>;
// tslint:disable-next-line: max-func-body-length
export const MessageRequestActionsConfirmation = ({ export const MessageRequestActionsConfirmation = ({
conversationType, conversationType,
i18n, i18n,
@ -37,10 +36,9 @@ export const MessageRequestActionsConfirmation = ({
profileName, profileName,
state, state,
title, title,
}: Props) => { }: Props): JSX.Element | null => {
if (state === MessageRequestState.blocking) { if (state === MessageRequestState.blocking) {
return ( return (
// tslint:disable-next-line: use-simple-attributes
<ConfirmationModal <ConfirmationModal
i18n={i18n} i18n={i18n}
onClose={() => { onClose={() => {
@ -82,7 +80,6 @@ export const MessageRequestActionsConfirmation = ({
if (state === MessageRequestState.unblocking) { if (state === MessageRequestState.unblocking) {
return ( return (
// tslint:disable-next-line: use-simple-attributes
<ConfirmationModal <ConfirmationModal
i18n={i18n} i18n={i18n}
onClose={() => { onClose={() => {
@ -91,7 +88,7 @@ export const MessageRequestActionsConfirmation = ({
title={ title={
<Intl <Intl
i18n={i18n} i18n={i18n}
id={'MessageRequests--unblock-confirm-title'} id="MessageRequests--unblock-confirm-title"
components={[ components={[
<ContactName <ContactName
key="name" key="name"
@ -119,7 +116,6 @@ export const MessageRequestActionsConfirmation = ({
if (state === MessageRequestState.deleting) { if (state === MessageRequestState.deleting) {
return ( return (
// tslint:disable-next-line: use-simple-attributes
<ConfirmationModal <ConfirmationModal
i18n={i18n} i18n={i18n}
onClose={() => { onClose={() => {

View file

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

View file

@ -9,11 +9,9 @@ import { pngUrl } from '../../storybook/Fixtures';
import { Message, Props as MessagesProps } from './Message'; import { Message, Props as MessagesProps } from './Message';
import { AUDIO_MP3, IMAGE_PNG, MIMEType, VIDEO_MP4 } from '../../types/MIME'; import { AUDIO_MP3, IMAGE_PNG, MIMEType, VIDEO_MP4 } from '../../types/MIME';
import { Props, Quote } from './Quote'; import { Props, Quote } from './Quote';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/Quote', module); const story = storiesOf('Components/Conversation/Quote', module);
@ -63,7 +61,7 @@ const renderInMessage = ({
}: Props) => { }: Props) => {
const messageProps = { const messageProps = {
...defaultMessageProps, ...defaultMessageProps,
authorColor: authorColor, authorColor,
quote: { quote: {
attachment, attachment,
authorId: 'an-author', authorId: 'an-author',
@ -186,6 +184,7 @@ story.add('Image Only', () => {
}, },
}, },
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props.text = undefined as any; props.text = undefined as any;
return <Quote {...props} />; return <Quote {...props} />;
@ -230,6 +229,7 @@ story.add('Video Only', () => {
}, },
}, },
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props.text = undefined as any; props.text = undefined as any;
return <Quote {...props} />; return <Quote {...props} />;
@ -271,6 +271,7 @@ story.add('Audio Only', () => {
isVoiceMessage: false, isVoiceMessage: false,
}, },
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props.text = undefined as any; props.text = undefined as any;
return <Quote {...props} />; return <Quote {...props} />;
@ -296,6 +297,7 @@ story.add('Voice Message Only', () => {
isVoiceMessage: true, isVoiceMessage: true,
}, },
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props.text = undefined as any; props.text = undefined as any;
return <Quote {...props} />; return <Quote {...props} />;
@ -321,6 +323,7 @@ story.add('Other File Only', () => {
isVoiceMessage: false, isVoiceMessage: false,
}, },
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props.text = undefined as any; props.text = undefined as any;
return <Quote {...props} />; return <Quote {...props} />;
@ -355,6 +358,7 @@ story.add('Message Not Found', () => {
story.add('Missing Text & Attachment', () => { story.add('Missing Text & Attachment', () => {
const props = createProps(); const props = createProps();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props.text = undefined as any; props.text = undefined as any;
return <Quote {...props} />; return <Quote {...props} />;

View file

@ -1,10 +1,8 @@
// tslint:disable:react-this-binding-issue
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import * as MIME from '../../../ts/types/MIME'; import * as MIME from '../../types/MIME';
import * as GoogleChrome from '../../../ts/util/GoogleChrome'; import * as GoogleChrome from '../../util/GoogleChrome';
import { MessageBody } from './MessageBody'; import { MessageBody } from './MessageBody';
import { BodyRangesType, LocalizerType } from '../../types/Util'; import { BodyRangesType, LocalizerType } from '../../types/Util';
@ -65,7 +63,7 @@ function getObjectUrl(thumbnail: Attachment | undefined): string | undefined {
return thumbnail.objectUrl; return thumbnail.objectUrl;
} }
return; return undefined;
} }
function getTypeLabel({ function getTypeLabel({
@ -86,19 +84,21 @@ function getTypeLabel({
if (MIME.isAudio(contentType) && isVoiceMessage) { if (MIME.isAudio(contentType) && isVoiceMessage) {
return i18n('voiceMessage'); return i18n('voiceMessage');
} }
if (MIME.isAudio(contentType)) {
return i18n('audio');
}
return; return MIME.isAudio(contentType) ? i18n('audio') : undefined;
} }
export class Quote extends React.Component<Props, State> { export class Quote extends React.Component<Props, State> {
public state = { constructor(props: Props) {
super(props);
this.state = {
imageBroken: false, imageBroken: false,
}; };
}
public handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => { public handleKeyDown = (
event: React.KeyboardEvent<HTMLButtonElement>
): void => {
const { onClick } = this.props; const { onClick } = this.props;
// This is important to ensure that using this quote to navigate to the referenced // This is important to ensure that using this quote to navigate to the referenced
@ -109,7 +109,8 @@ export class Quote extends React.Component<Props, State> {
onClick(); onClick();
} }
}; };
public handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
public handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
const { onClick } = this.props; const { onClick } = this.props;
if (onClick) { if (onClick) {
@ -119,15 +120,20 @@ export class Quote extends React.Component<Props, State> {
} }
}; };
public handleImageError = () => { public handleImageError = (): void => {
// tslint:disable-next-line no-console window.console.info(
console.log('Message: Image failed to load; failing over to placeholder'); 'Message: Image failed to load; failing over to placeholder'
);
this.setState({ this.setState({
imageBroken: true, imageBroken: true,
}); });
}; };
public renderImage(url: string, i18n: LocalizerType, icon?: string) { public renderImage(
url: string,
i18n: LocalizerType,
icon?: string
): JSX.Element {
const iconElement = icon ? ( const iconElement = icon ? (
<div className="module-quote__icon-container__inner"> <div className="module-quote__icon-container__inner">
<div className="module-quote__icon-container__circle-background"> <div className="module-quote__icon-container__circle-background">
@ -153,7 +159,8 @@ export class Quote extends React.Component<Props, State> {
); );
} }
public renderIcon(icon: string) { // eslint-disable-next-line class-methods-use-this
public renderIcon(icon: string): JSX.Element {
return ( return (
<div className="module-quote__icon-container"> <div className="module-quote__icon-container">
<div className="module-quote__icon-container__inner"> <div className="module-quote__icon-container__inner">
@ -170,11 +177,11 @@ export class Quote extends React.Component<Props, State> {
); );
} }
public renderGenericFile() { public renderGenericFile(): JSX.Element | null {
const { attachment, isIncoming } = this.props; const { attachment, isIncoming } = this.props;
if (!attachment) { if (!attachment) {
return; return null;
} }
const { fileName, contentType } = attachment; const { fileName, contentType } = attachment;
@ -202,7 +209,7 @@ export class Quote extends React.Component<Props, State> {
); );
} }
public renderIconContainer() { public renderIconContainer(): JSX.Element | null {
const { attachment, i18n } = this.props; const { attachment, i18n } = this.props;
const { imageBroken } = this.state; const { imageBroken } = this.state;
@ -283,8 +290,8 @@ export class Quote extends React.Component<Props, State> {
return null; return null;
} }
public renderClose() { public renderClose(): JSX.Element | null {
const { onClose } = this.props; const { i18n, onClose } = this.props;
if (!onClose) { if (!onClose) {
return null; return null;
@ -313,6 +320,7 @@ export class Quote extends React.Component<Props, State> {
// We can't be a button because the overall quote is a button; can't nest them // We can't be a button because the overall quote is a button; can't nest them
role="button" role="button"
className="module-quote__close-button" className="module-quote__close-button"
aria-label={i18n('close')}
onKeyDown={keyDownHandler} onKeyDown={keyDownHandler}
onClick={clickHandler} onClick={clickHandler}
/> />
@ -320,7 +328,7 @@ export class Quote extends React.Component<Props, State> {
); );
} }
public renderAuthor() { public renderAuthor(): JSX.Element {
const { const {
authorProfileName, authorProfileName,
authorPhoneNumber, authorPhoneNumber,
@ -353,7 +361,7 @@ export class Quote extends React.Component<Props, State> {
); );
} }
public renderReferenceWarning() { public renderReferenceWarning(): JSX.Element | null {
const { i18n, isIncoming, referencedMessageNotFound } = this.props; const { i18n, isIncoming, referencedMessageNotFound } = this.props;
if (!referencedMessageNotFound) { if (!referencedMessageNotFound) {
@ -389,7 +397,7 @@ export class Quote extends React.Component<Props, State> {
); );
} }
public render() { public render(): JSX.Element | null {
const { const {
authorColor, authorColor,
isIncoming, isIncoming,
@ -410,6 +418,7 @@ export class Quote extends React.Component<Props, State> {
)} )}
> >
<button <button
type="button"
onClick={this.handleClick} onClick={this.handleClick}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
className={classNames( className={classNames(

View file

@ -65,6 +65,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
return ( return (
<button <button
type="button"
key={emoji} key={emoji}
ref={maybeFocusRef} ref={maybeFocusRef}
tabIndex={0} tabIndex={0}
@ -87,6 +88,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
); );
})} })}
<button <button
type="button"
className={classNames( className={classNames(
'module-reaction-picker__emoji-btn', 'module-reaction-picker__emoji-btn',
otherSelected otherSelected

View file

@ -4,11 +4,9 @@ import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { Props, ReactionViewer } from './ReactionViewer'; import { Props, ReactionViewer } from './ReactionViewer';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/ReactionViewer', module); const story = storiesOf('Components/Conversation/ReactionViewer', module);

View file

@ -35,7 +35,6 @@ export type Props = OwnProps &
const emojisOrder = ['❤️', '👍', '👎', '😂', '😮', '😢', '😡']; const emojisOrder = ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'];
export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>( export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
// tslint:disable-next-line max-func-body-length
({ i18n, reactions, onClose, pickedReaction, ...rest }, ref) => { ({ i18n, reactions, onClose, pickedReaction, ...rest }, ref) => {
const grouped = mapValues(groupBy(reactions, 'emoji'), res => const grouped = mapValues(groupBy(reactions, 'emoji'), res =>
orderBy(res, ['timestamp'], ['desc']) orderBy(res, ['timestamp'], ['desc'])
@ -112,6 +111,7 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
return ( return (
<button <button
type="button"
key={cat} key={cat}
ref={maybeFocusRef} ref={maybeFocusRef}
className={classNames( className={classNames(

View file

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

View file

@ -6,14 +6,8 @@ export interface Props {
i18n: LocalizerType; i18n: LocalizerType;
} }
export class ResetSessionNotification extends React.Component<Props> { export const ResetSessionNotification = ({ i18n }: Props): JSX.Element => (
public render() {
const { i18n } = this.props;
return (
<div className="module-reset-session-notification"> <div className="module-reset-session-notification">
{i18n('sessionEnded')} {i18n('sessionEnded')}
</div> </div>
); );
}
}

View file

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

View file

@ -27,9 +27,12 @@ export type PropsActions = {
export type Props = PropsData & PropsHousekeeping & PropsActions; export type Props = PropsData & PropsHousekeeping & PropsActions;
export class SafetyNumberNotification extends React.Component<Props> { export const SafetyNumberNotification = ({
public render() { contact,
const { contact, isGroup, i18n, showIdentity } = this.props; isGroup,
i18n,
showIdentity,
}: Props): JSX.Element => {
const changeKey = isGroup const changeKey = isGroup
? 'safetyNumberChangedGroup' ? 'safetyNumberChangedGroup'
: 'safetyNumberChanged'; : 'safetyNumberChanged';
@ -59,6 +62,7 @@ export class SafetyNumberNotification extends React.Component<Props> {
/> />
</div> </div>
<button <button
type="button"
onClick={() => { onClick={() => {
showIdentity(contact.id); showIdentity(contact.id);
}} }}
@ -68,5 +72,4 @@ export class SafetyNumberNotification extends React.Component<Props> {
</button> </button>
</div> </div>
); );
} };
}

View file

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

View file

@ -12,16 +12,18 @@ export type Props = {
i18n: LocalizerType; i18n: LocalizerType;
}; };
export class ScrollDownButton extends React.Component<Props> { export const ScrollDownButton = ({
public render() { conversationId,
const { conversationId, withNewMessages, i18n, scrollDown } = this.props; withNewMessages,
const altText = withNewMessages i18n,
? i18n('messagesBelow') scrollDown,
: i18n('scrollDown'); }: Props): JSX.Element => {
const altText = withNewMessages ? i18n('messagesBelow') : i18n('scrollDown');
return ( return (
<div className="module-scroll-down"> <div className="module-scroll-down">
<button <button
type="button"
className={classNames( className={classNames(
'module-scroll-down__button', 'module-scroll-down__button',
withNewMessages ? 'module-scroll-down__button--new-messages' : null withNewMessages ? 'module-scroll-down__button--new-messages' : null
@ -35,5 +37,4 @@ export class ScrollDownButton extends React.Component<Props> {
</button> </button>
</div> </div>
); );
} };
}

View file

@ -5,13 +5,8 @@ import { action } from '@storybook/addon-actions';
import { AttachmentType } from '../../types/Attachment'; import { AttachmentType } from '../../types/Attachment';
import { MIMEType } from '../../types/MIME'; import { MIMEType } from '../../types/MIME';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { Props, StagedGenericAttachment } from './StagedGenericAttachment'; import { Props, StagedGenericAttachment } from './StagedGenericAttachment';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -9,16 +9,20 @@ export interface Props {
i18n: LocalizerType; i18n: LocalizerType;
} }
export class StagedGenericAttachment extends React.Component<Props> { export const StagedGenericAttachment = ({
public render() { attachment,
const { attachment, onClose } = this.props; i18n,
onClose,
}: Props): JSX.Element => {
const { fileName, contentType } = attachment; const { fileName, contentType } = attachment;
const extension = getExtensionForDisplay({ contentType, fileName }); const extension = getExtensionForDisplay({ contentType, fileName });
return ( return (
<div className="module-staged-generic-attachment"> <div className="module-staged-generic-attachment">
<button <button
type="button"
className="module-staged-generic-attachment__close-button" className="module-staged-generic-attachment__close-button"
aria-label={i18n('close')}
onClick={() => { onClick={() => {
if (onClose) { if (onClose) {
onClose(attachment); onClose(attachment);
@ -37,5 +41,4 @@ export class StagedGenericAttachment extends React.Component<Props> {
</div> </div>
</div> </div>
); );
} };
}

View file

@ -5,19 +5,15 @@ import { action } from '@storybook/addon-actions';
import { AttachmentType } from '../../types/Attachment'; import { AttachmentType } from '../../types/Attachment';
import { MIMEType } from '../../types/MIME'; import { MIMEType } from '../../types/MIME';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { Props, StagedLinkPreview } from './StagedLinkPreview'; import { Props, StagedLinkPreview } from './StagedLinkPreview';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/StagedLinkPreview', module); const story = storiesOf('Components/Conversation/StagedLinkPreview', module);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
story.addDecorator((withKnobs as any)({ escapeHTML: false })); story.addDecorator((withKnobs as any)({ escapeHTML: false }));
const createAttachment = ( const createAttachment = (

View file

@ -16,10 +16,14 @@ export interface Props {
onClose?: () => void; onClose?: () => void;
} }
export class StagedLinkPreview extends React.Component<Props> { export const StagedLinkPreview = ({
public render() { isLoaded,
const { isLoaded, onClose, i18n, title, image, domain } = this.props; onClose,
i18n,
title,
image,
domain,
}: Props): JSX.Element => {
const isImage = image && isImageAttachment(image); const isImage = image && isImageAttachment(image);
return ( return (
@ -38,7 +42,7 @@ export class StagedLinkPreview extends React.Component<Props> {
<div className="module-staged-link-preview__icon-container"> <div className="module-staged-link-preview__icon-container">
<Image <Image
alt={i18n('stagedPreviewThumbnail', [domain])} alt={i18n('stagedPreviewThumbnail', [domain])}
softCorners={true} softCorners
height={72} height={72}
width={72} width={72}
url={image.url} url={image.url}
@ -54,10 +58,11 @@ export class StagedLinkPreview extends React.Component<Props> {
</div> </div>
) : null} ) : null}
<button <button
type="button"
className="module-staged-link-preview__close-button" className="module-staged-link-preview__close-button"
onClick={onClose} onClick={onClose}
aria-label={i18n('close')}
/> />
</div> </div>
); );
} };
}

View file

@ -2,12 +2,8 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; 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 { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment'; import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);

View file

@ -6,18 +6,16 @@ interface Props {
i18n: LocalizerType; i18n: LocalizerType;
} }
export class StagedPlaceholderAttachment extends React.Component<Props> { export const StagedPlaceholderAttachment = ({
public render() { i18n,
const { i18n, onClick } = this.props; onClick,
}: Props): JSX.Element => (
return (
<button <button
type="button"
className="module-staged-placeholder-attachment" className="module-staged-placeholder-attachment"
onClick={onClick} onClick={onClick}
title={i18n('add-image-attachment')} title={i18n('add-image-attachment')}
> >
<div className="module-staged-placeholder-attachment__plus-icon" /> <div className="module-staged-placeholder-attachment__plus-icon" />
</button> </button>
); );
}
}

View file

@ -3,12 +3,8 @@ import { storiesOf } from '@storybook/react';
import { boolean, number } from '@storybook/addon-knobs'; import { boolean, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { Props, Timeline } from './Timeline'; import { Props, Timeline } from './Timeline';
import { TimelineItem, TimelineItemType } from './TimelineItem'; import { TimelineItem, TimelineItemType } from './TimelineItem';
import { LastSeenIndicator } from './LastSeenIndicator'; import { LastSeenIndicator } from './LastSeenIndicator';
@ -19,7 +15,7 @@ const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/Timeline', module); const story = storiesOf('Components/Conversation/Timeline', module);
// tslint:disable-next-line // eslint-disable-next-line
const noop = () => {}; const noop = () => {};
Object.assign(window, { Object.assign(window, {
@ -207,6 +203,7 @@ const items: Record<string, TimelineItemType> = {
type: 'linkNotification', type: 'linkNotification',
data: null, data: null,
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any; } as any;
const actions = () => ({ const actions = () => ({

View file

@ -1,10 +1,11 @@
import { debounce, get, isNumber } from 'lodash'; import { debounce, get, isNumber } from 'lodash';
import React from 'react'; import React, { CSSProperties } from 'react';
import { import {
AutoSizer, AutoSizer,
CellMeasurer, CellMeasurer,
CellMeasurerCache, CellMeasurerCache,
List, List,
Grid,
} from 'react-virtualized'; } from 'react-virtualized';
import { ScrollDownButton } from './ScrollDownButton'; import { ScrollDownButton } from './ScrollDownButton';
@ -39,7 +40,7 @@ export type PropsDataType = {
type PropsHousekeepingType = { type PropsHousekeepingType = {
id: string; id: string;
unreadCount?: number; unreadCount?: number;
typingContact?: Object; typingContact?: unknown;
selectedMessageId?: string; selectedMessageId?: string;
i18n: LocalizerType; i18n: LocalizerType;
@ -47,7 +48,7 @@ type PropsHousekeepingType = {
renderItem: ( renderItem: (
id: string, id: string,
conversationId: string, conversationId: string,
actions: Object actions: Record<string, unknown>
) => JSX.Element; ) => JSX.Element;
renderLastSeenIndicator: (id: string) => JSX.Element; renderLastSeenIndicator: (id: string) => JSX.Element;
renderHeroRow: ( renderHeroRow: (
@ -86,8 +87,8 @@ type RowRendererParamsType = {
isScrolling: boolean; isScrolling: boolean;
isVisible: boolean; isVisible: boolean;
key: string; key: string;
parent: Object; parent: Record<string, unknown>;
style: Object; style: CSSProperties;
}; };
type OnScrollParamsType = { type OnScrollParamsType = {
scrollTop: number; scrollTop: number;
@ -134,13 +135,20 @@ export class Timeline extends React.PureComponent<Props, State> {
defaultHeight: 64, defaultHeight: 64,
fixedWidth: true, fixedWidth: true,
}); });
public mostRecentWidth = 0; public mostRecentWidth = 0;
public mostRecentHeight = 0; public mostRecentHeight = 0;
public offsetFromBottom: number | undefined = 0; public offsetFromBottom: number | undefined = 0;
public resizeFlag = false; public resizeFlag = false;
public listRef = React.createRef<any>();
public listRef = React.createRef<List>();
public visibleRows: VisibleRowsType | undefined; public visibleRows: VisibleRowsType | undefined;
public loadCountdownTimeout: any;
public loadCountdownTimeout: NodeJS.Timeout | null = null;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -176,9 +184,9 @@ export class Timeline extends React.PureComponent<Props, State> {
return state; return state;
} }
public getList = () => { public getList = (): List | null => {
if (!this.listRef) { if (!this.listRef) {
return; return null;
} }
const { current } = this.listRef; const { current } = this.listRef;
@ -186,25 +194,30 @@ export class Timeline extends React.PureComponent<Props, State> {
return current; return current;
}; };
public getGrid = () => { public getGrid = (): Grid | undefined => {
const list = this.getList(); const list = this.getList();
if (!list) { if (!list) {
return; return;
} }
// eslint-disable-next-line consistent-return
return list.Grid; return list.Grid;
}; };
public getScrollContainer = () => { public getScrollContainer = (): HTMLDivElement | undefined => {
const grid = this.getGrid(); // 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 = this.getGrid();
if (!grid) { if (!grid) {
return; return;
} }
// eslint-disable-next-line consistent-return
return grid._scrollingContainer as HTMLDivElement; return grid._scrollingContainer as HTMLDivElement;
}; };
public scrollToRow = (row: number) => { public scrollToRow = (row: number): void => {
const list = this.getList(); const list = this.getList();
if (!list) { if (!list) {
return; return;
@ -213,7 +226,7 @@ export class Timeline extends React.PureComponent<Props, State> {
list.scrollToRow(row); list.scrollToRow(row);
}; };
public recomputeRowHeights = (row?: number) => { public recomputeRowHeights = (row?: number): void => {
const list = this.getList(); const list = this.getList();
if (!list) { if (!list) {
return; return;
@ -222,7 +235,7 @@ export class Timeline extends React.PureComponent<Props, State> {
list.recomputeRowHeights(row); list.recomputeRowHeights(row);
}; };
public onHeightOnlyChange = () => { public onHeightOnlyChange = (): void => {
const grid = this.getGrid(); const grid = this.getGrid();
const scrollContainer = this.getScrollContainer(); const scrollContainer = this.getScrollContainer();
if (!grid || !scrollContainer) { if (!grid || !scrollContainer) {
@ -240,13 +253,18 @@ export class Timeline extends React.PureComponent<Props, State> {
); );
const delta = newOffsetFromBottom - this.offsetFromBottom; const delta = newOffsetFromBottom - this.offsetFromBottom;
grid.scrollToPosition({ scrollTop: scrollContainer.scrollTop + delta }); // TODO: DESKTOP-687
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(grid as any).scrollToPosition({
scrollTop: scrollContainer.scrollTop + delta,
});
}; };
public resize = (row?: number) => { public resize = (row?: number): void => {
this.offsetFromBottom = undefined; this.offsetFromBottom = undefined;
this.resizeFlag = false; this.resizeFlag = false;
if (isNumber(row) && row > 0) { if (isNumber(row) && row > 0) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.cellSizeCache.clearPlus(row, 0); this.cellSizeCache.clearPlus(row, 0);
} else { } else {
@ -256,11 +274,11 @@ export class Timeline extends React.PureComponent<Props, State> {
this.recomputeRowHeights(row || 0); this.recomputeRowHeights(row || 0);
}; };
public resizeHeroRow = () => { public resizeHeroRow = (): void => {
this.resize(0); this.resize(0);
}; };
public onScroll = (data: OnScrollParamsType) => { public onScroll = (data: OnScrollParamsType): void => {
// Ignore scroll events generated as react-virtualized recursively scrolls and // Ignore scroll events generated as react-virtualized recursively scrolls and
// re-measures to get us where we want to go. // re-measures to get us where we want to go.
if ( if (
@ -284,7 +302,6 @@ export class Timeline extends React.PureComponent<Props, State> {
this.updateWithVisibleRows(); this.updateWithVisibleRows();
}; };
// tslint:disable-next-line member-ordering
public updateScrollMetrics = debounce( public updateScrollMetrics = debounce(
(data: OnScrollParamsType) => { (data: OnScrollParamsType) => {
const { clientHeight, clientWidth, scrollHeight, scrollTop } = data; const { clientHeight, clientWidth, scrollHeight, scrollTop } = data;
@ -337,10 +354,14 @@ export class Timeline extends React.PureComponent<Props, State> {
); );
} }
// Variable collision
// eslint-disable-next-line react/destructuring-assignment
if (loadCountdownStart !== this.props.loadCountdownStart) { if (loadCountdownStart !== this.props.loadCountdownStart) {
setLoadCountdownStart(id, loadCountdownStart); setLoadCountdownStart(id, loadCountdownStart);
} }
// Variable collision
// eslint-disable-next-line react/destructuring-assignment
if (isNearBottom !== this.props.isNearBottom) { if (isNearBottom !== this.props.isNearBottom) {
setIsNearBottom(id, isNearBottom); setIsNearBottom(id, isNearBottom);
} }
@ -356,7 +377,7 @@ export class Timeline extends React.PureComponent<Props, State> {
{ maxWait: 50 } { maxWait: 50 }
); );
public updateVisibleRows = () => { public updateVisibleRows = (): void => {
let newest; let newest;
let oldest; let oldest;
@ -384,6 +405,7 @@ export class Timeline extends React.PureComponent<Props, State> {
const { id, offsetTop, offsetHeight } = child; const { id, offsetTop, offsetHeight } = child;
if (!id) { if (!id) {
// eslint-disable-next-line no-continue
continue; continue;
} }
@ -403,6 +425,7 @@ export class Timeline extends React.PureComponent<Props, State> {
const { offsetTop, id } = child; const { offsetTop, id } = child;
if (!id) { if (!id) {
// eslint-disable-next-line no-continue
continue; continue;
} }
@ -417,7 +440,6 @@ export class Timeline extends React.PureComponent<Props, State> {
this.visibleRows = { newest, oldest }; this.visibleRows = { newest, oldest };
}; };
// tslint:disable-next-line member-ordering cyclomatic-complexity
public updateWithVisibleRows = debounce( public updateWithVisibleRows = debounce(
() => { () => {
const { const {
@ -479,7 +501,7 @@ export class Timeline extends React.PureComponent<Props, State> {
{ maxWait: 500 } { maxWait: 500 }
); );
public loadOlderMessages = () => { public loadOlderMessages = (): void => {
const { const {
haveOldest, haveOldest,
isLoadingMessages, isLoadingMessages,
@ -505,7 +527,7 @@ export class Timeline extends React.PureComponent<Props, State> {
key, key,
parent, parent,
style, style,
}: RowRendererParamsType) => { }: RowRendererParamsType): JSX.Element => {
const { const {
id, id,
haveOldest, haveOldest,
@ -591,7 +613,7 @@ export class Timeline extends React.PureComponent<Props, State> {
); );
}; };
public fromItemIndexToRow(index: number) { public fromItemIndexToRow(index: number): number {
const { oldestUnreadIndex } = this.props; const { oldestUnreadIndex } = this.props;
// We will always render either the hero row or the loading row // We will always render either the hero row or the loading row
@ -604,7 +626,7 @@ export class Timeline extends React.PureComponent<Props, State> {
return index + addition; return index + addition;
} }
public getRowCount() { public getRowCount(): number {
const { oldestUnreadIndex, typingContact } = this.props; const { oldestUnreadIndex, typingContact } = this.props;
const { items } = this.props; const { items } = this.props;
const itemsCount = items && items.length ? items.length : 0; const itemsCount = items && items.length ? items.length : 0;
@ -639,19 +661,21 @@ export class Timeline extends React.PureComponent<Props, State> {
return; return;
} }
// eslint-disable-next-line consistent-return
return index; return index;
} }
public getLastSeenIndicatorRow(props?: Props) { public getLastSeenIndicatorRow(props?: Props): number | undefined {
const { oldestUnreadIndex } = props || this.props; const { oldestUnreadIndex } = props || this.props;
if (!isNumber(oldestUnreadIndex)) { if (!isNumber(oldestUnreadIndex)) {
return; return;
} }
// eslint-disable-next-line consistent-return
return this.fromItemIndexToRow(oldestUnreadIndex) - 1; return this.fromItemIndexToRow(oldestUnreadIndex) - 1;
} }
public getTypingBubbleRow() { public getTypingBubbleRow(): number | undefined {
const { items } = this.props; const { items } = this.props;
if (!items || items.length < 0) { if (!items || items.length < 0) {
return; return;
@ -659,10 +683,11 @@ export class Timeline extends React.PureComponent<Props, State> {
const last = items.length - 1; const last = items.length - 1;
// eslint-disable-next-line consistent-return
return this.fromItemIndexToRow(last) + 1; return this.fromItemIndexToRow(last) + 1;
} }
public onScrollToMessage = (messageId: string) => { public onScrollToMessage = (messageId: string): void => {
const { isLoadingMessages, items, loadAndScroll } = this.props; const { isLoadingMessages, items, loadAndScroll } = this.props;
const index = items.findIndex(item => item === messageId); const index = items.findIndex(item => item === messageId);
@ -678,7 +703,7 @@ export class Timeline extends React.PureComponent<Props, State> {
} }
}; };
public scrollToBottom = (setFocus?: boolean) => { public scrollToBottom = (setFocus?: boolean): void => {
const { selectMessage, id, items } = this.props; const { selectMessage, id, items } = this.props;
if (setFocus && items && items.length > 0) { if (setFocus && items && items.length > 0) {
@ -694,11 +719,11 @@ export class Timeline extends React.PureComponent<Props, State> {
}); });
}; };
public onClickScrollDownButton = () => { public onClickScrollDownButton = (): void => {
this.scrollDown(false); this.scrollDown(false);
}; };
public scrollDown = (setFocus?: boolean) => { public scrollDown = (setFocus?: boolean): void => {
const { const {
haveNewest, haveNewest,
id, id,
@ -746,19 +771,20 @@ export class Timeline extends React.PureComponent<Props, State> {
} }
}; };
public componentDidMount() { public componentDidMount(): void {
this.updateWithVisibleRows(); this.updateWithVisibleRows();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.registerForActive(this.updateWithVisibleRows); window.registerForActive(this.updateWithVisibleRows);
} }
public componentWillUnmount() { public componentWillUnmount(): void {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.unregisterForActive(this.updateWithVisibleRows); window.unregisterForActive(this.updateWithVisibleRows);
} }
// tslint:disable-next-line cyclomatic-complexity max-func-body-length public componentDidUpdate(prevProps: Props): void {
public componentDidUpdate(prevProps: Props) {
const { const {
id, id,
clearChangedMessages, clearChangedMessages,
@ -787,6 +813,8 @@ export class Timeline extends React.PureComponent<Props, State> {
} }
const oneTimeScrollRow = this.getLastSeenIndicatorRow(); const oneTimeScrollRow = this.getLastSeenIndicatorRow();
// TODO: DESKTOP-688
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ this.setState({
oneTimeScrollRow, oneTimeScrollRow,
atBottom: true, atBottom: true,
@ -804,7 +832,9 @@ export class Timeline extends React.PureComponent<Props, State> {
prevProps.items.length > 0 && prevProps.items.length > 0 &&
items !== prevProps.items items !== prevProps.items
) { ) {
if (this.state.atTop) { const { atTop } = this.state;
if (atTop) {
const oldFirstIndex = 0; const oldFirstIndex = 0;
const oldFirstId = prevProps.items[oldFirstIndex]; const oldFirstId = prevProps.items[oldFirstIndex];
@ -820,6 +850,8 @@ export class Timeline extends React.PureComponent<Props, State> {
if (delta > 0) { if (delta > 0) {
// We're loading more new messages at the top; we want to stay at the top // We're loading more new messages at the top; we want to stay at the top
this.resize(); this.resize();
// TODO: DESKTOP-688
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ oneTimeScrollRow: newRow }); this.setState({ oneTimeScrollRow: newRow });
return; return;
@ -900,7 +932,7 @@ export class Timeline extends React.PureComponent<Props, State> {
this.updateWithVisibleRows(); this.updateWithVisibleRows();
} }
public getScrollTarget = () => { public getScrollTarget = (): number | undefined => {
const { oneTimeScrollRow, atBottom, propScrollToIndex } = this.state; const { oneTimeScrollRow, atBottom, propScrollToIndex } = this.state;
const rowCount = this.getRowCount(); const rowCount = this.getRowCount();
@ -920,7 +952,7 @@ export class Timeline extends React.PureComponent<Props, State> {
return scrollToBottom; return scrollToBottom;
}; };
public handleBlur = (event: React.FocusEvent) => { public handleBlur = (event: React.FocusEvent): void => {
const { clearSelectedMessage } = this.props; const { clearSelectedMessage } = this.props;
const { currentTarget } = event; const { currentTarget } = event;
@ -944,7 +976,7 @@ export class Timeline extends React.PureComponent<Props, State> {
}, 0); }, 0);
}; };
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => { public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
const { selectMessage, selectedMessageId, items, id } = this.props; const { selectMessage, selectedMessageId, items, id } = this.props;
const commandKey = get(window, 'platform') === 'darwin' && event.metaKey; const commandKey = get(window, 'platform') === 'darwin' && event.metaKey;
const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey; const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey;
@ -1015,12 +1047,10 @@ export class Timeline extends React.PureComponent<Props, State> {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return;
} }
}; };
public render() { public render(): JSX.Element | null {
const { i18n, id, items } = this.props; const { i18n, id, items } = this.props;
const { const {
shouldShowScrollDownButton, shouldShowScrollDownButton,
@ -1037,7 +1067,7 @@ export class Timeline extends React.PureComponent<Props, State> {
return ( return (
<div <div
className="module-timeline" className="module-timeline"
role="group" role="presentation"
tabIndex={-1} tabIndex={-1}
onBlur={this.handleBlur} onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
@ -1062,6 +1092,7 @@ export class Timeline extends React.PureComponent<Props, State> {
<List <List
deferredMeasurementCache={this.cellSizeCache} deferredMeasurementCache={this.cellSizeCache}
height={height} height={height}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onScroll={this.onScroll as any} onScroll={this.onScroll as any}
overscanRowCount={10} overscanRowCount={10}
ref={this.listRef} ref={this.listRef}

View file

@ -2,13 +2,10 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { EmojiPicker } from '../emoji/EmojiPicker'; import { EmojiPicker } from '../emoji/EmojiPicker';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem'; import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -80,7 +77,6 @@ storiesOf('Components/Conversation/TimelineItem', module)
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />; return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
}) })
// tslint:disable-next-line max-func-body-length
.add('Notification', () => { .add('Notification', () => {
const items = [ const items = [
{ {
@ -173,7 +169,7 @@ storiesOf('Components/Conversation/TimelineItem', module)
acceptedTime: Date.now() - 200, acceptedTime: Date.now() - 200,
wasDeclined: false, wasDeclined: false,
wasIncoming: false, wasIncoming: false,
wasVideoCall: true, wasVideoCall: false,
endedTime: Date.now(), endedTime: Date.now(),
}, },
}, },
@ -193,8 +189,8 @@ storiesOf('Components/Conversation/TimelineItem', module)
}, },
{ {
type: 'callHistory', type: 'callHistory',
callHistoryDetails: {
data: { data: {
callHistoryDetails: {
// declined outgoing audio // declined outgoing audio
wasDeclined: true, wasDeclined: true,
wasIncoming: false, wasIncoming: false,
@ -243,20 +239,21 @@ storiesOf('Components/Conversation/TimelineItem', module)
return ( return (
<> <>
{items.map(item => ( {items.map((item, index) => (
<> <React.Fragment key={index}>
<TimelineItem <TimelineItem
{...getDefaultProps()} {...getDefaultProps()}
item={item as TimelineItemProps['item']} item={item as TimelineItemProps['item']}
i18n={i18n} i18n={i18n}
/> />
<hr /> <hr />
</> </React.Fragment>
))} ))}
</> </>
); );
}) })
.add('Unknown Type', () => { .add('Unknown Type', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: intentional // @ts-ignore: intentional
const item = { const item = {
type: 'random', type: 'random',
@ -268,6 +265,7 @@ storiesOf('Components/Conversation/TimelineItem', module)
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />; return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
}) })
.add('Missing Item', () => { .add('Missing Item', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: intentional // @ts-ignore: intentional
const item = null as TimelineItemProps['item']; const item = null as TimelineItemProps['item'];

View file

@ -124,7 +124,7 @@ export type PropsType = PropsLocalType &
Pick<AllMessageProps, 'renderEmojiPicker'>; Pick<AllMessageProps, 'renderEmojiPicker'>;
export class TimelineItem extends React.PureComponent<PropsType> { export class TimelineItem extends React.PureComponent<PropsType> {
public render() { public render(): JSX.Element | null {
const { const {
conversationId, conversationId,
id, id,
@ -136,8 +136,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
} = this.props; } = this.props;
if (!item) { if (!item) {
// tslint:disable-next-line:no-console window.log.warn(`TimelineItem: item ${id} provided was falsey`);
console.warn(`TimelineItem: item ${id} provided was falsey`);
return null; return null;
} }

View file

@ -16,18 +16,15 @@ export type Props = {
const FAKE_DURATION = 1000; const FAKE_DURATION = 1000;
export class TimelineLoadingRow extends React.PureComponent<Props> { export class TimelineLoadingRow extends React.PureComponent<Props> {
public renderContents() { public renderContents(): JSX.Element {
const { state, duration, expiresAt, onComplete } = this.props; const { state, duration, expiresAt, onComplete } = this.props;
if (state === 'idle') { if (state === 'idle') {
const fakeExpiresAt = Date.now() - FAKE_DURATION; const fakeExpiresAt = Date.now() - FAKE_DURATION;
return <Countdown duration={FAKE_DURATION} expiresAt={fakeExpiresAt} />; return <Countdown duration={FAKE_DURATION} expiresAt={fakeExpiresAt} />;
} else if ( }
state === 'countdown' && if (state === 'countdown' && isNumber(duration) && isNumber(expiresAt)) {
isNumber(duration) &&
isNumber(expiresAt)
) {
return ( return (
<Countdown <Countdown
duration={duration} duration={duration}
@ -40,7 +37,7 @@ export class TimelineLoadingRow extends React.PureComponent<Props> {
return <Spinner size="24" svgSize="small" direction="on-background" />; return <Spinner size="24" svgSize="small" direction="on-background" />;
} }
public render() { public render(): JSX.Element {
return ( return (
<div className="module-timeline-loading-row">{this.renderContents()}</div> <div className="module-timeline-loading-row">{this.renderContents()}</div>
); );

View file

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

View file

@ -22,7 +22,7 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping; export type Props = PropsData & PropsHousekeeping;
export class TimerNotification extends React.Component<Props> { export class TimerNotification extends React.Component<Props> {
public renderContents() { public renderContents(): JSX.Element | string | null {
const { const {
i18n, i18n,
name, name,
@ -71,13 +71,13 @@ export class TimerNotification extends React.Component<Props> {
? i18n('disappearingMessagesDisabledByMember') ? i18n('disappearingMessagesDisabledByMember')
: i18n('timerSetByMember', [timespan]); : i18n('timerSetByMember', [timespan]);
default: default:
console.warn('TimerNotification: unsupported type provided:', type); window.log.warn('TimerNotification: unsupported type provided:', type);
return null; return null;
} }
} }
public render() { public render(): JSX.Element {
const { timespan, disabled } = this.props; const { timespan, disabled } = this.props;
return ( return (

View file

@ -2,19 +2,15 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean, date, select, text } from '@storybook/addon-knobs'; import { boolean, date, select, text } from '@storybook/addon-knobs';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { Props, Timestamp } from './Timestamp'; import { Props, Timestamp } from './Timestamp';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/Timestamp', module); const story = storiesOf('Components/Conversation/Timestamp', module);
const now = Date.now; const { now } = Date;
const seconds = (n: number) => n * 1000; const seconds = (n: number) => n * 1000;
const minutes = (n: number) => 60 * seconds(n); const minutes = (n: number) => 60 * seconds(n);
const hours = (n: number) => 60 * minutes(n); const hours = (n: number) => 60 * minutes(n);
@ -70,6 +66,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
const createTable = (overrideProps: Partial<Props> = {}) => ( const createTable = (overrideProps: Partial<Props> = {}) => (
<table cellPadding={5}> <table cellPadding={5}>
<tbody>
<tr> <tr>
<th>Description</th> <th>Description</th>
<th>Timestamp</th> <th>Timestamp</th>
@ -85,6 +82,7 @@ const createTable = (overrideProps: Partial<Props> = {}) => (
</td> </td>
</tr> </tr>
))} ))}
</tbody>
</table> </table>
); );

View file

@ -21,7 +21,7 @@ export interface Props {
const UPDATE_FREQUENCY = 60 * 1000; const UPDATE_FREQUENCY = 60 * 1000;
export class Timestamp extends React.Component<Props> { export class Timestamp extends React.Component<Props> {
private interval: any; private interval: NodeJS.Timeout | null;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -29,22 +29,24 @@ export class Timestamp extends React.Component<Props> {
this.interval = null; this.interval = null;
} }
public componentDidMount() { public componentDidMount(): void {
const update = () => { const update = () => {
this.setState({ this.setState({
// Used to trigger renders
// eslint-disable-next-line react/no-unused-state
lastUpdated: Date.now(), lastUpdated: Date.now(),
}); });
}; };
this.interval = setInterval(update, UPDATE_FREQUENCY); this.interval = setInterval(update, UPDATE_FREQUENCY);
} }
public componentWillUnmount() { public componentWillUnmount(): void {
if (this.interval) { if (this.interval) {
clearInterval(this.interval); clearInterval(this.interval);
} }
} }
public render() { public render(): JSX.Element | null {
const { const {
direction, direction,
i18n, i18n,

View file

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

View file

@ -8,11 +8,7 @@ export interface Props {
color?: string; color?: string;
} }
export class TypingAnimation extends React.Component<Props> { export const TypingAnimation = ({ i18n, color }: Props): JSX.Element => (
public render() {
const { i18n, color } = this.props;
return (
<div className="module-typing-animation" title={i18n('typingAlt')}> <div className="module-typing-animation" title={i18n('typingAlt')}>
<div <div
className={classNames( className={classNames(
@ -38,6 +34,4 @@ export class TypingAnimation extends React.Component<Props> {
)} )}
/> />
</div> </div>
); );
}
}

View file

@ -2,12 +2,8 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { select, text } from '@storybook/addon-knobs'; import { select, text } from '@storybook/addon-knobs';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { Props, TypingBubble } from './TypingBubble'; import { Props, TypingBubble } from './TypingBubble';
import { Colors } from '../../types/Colors'; import { Colors } from '../../types/Colors';

View file

@ -19,7 +19,7 @@ export interface Props {
} }
export class TypingBubble extends React.PureComponent<Props> { export class TypingBubble extends React.PureComponent<Props> {
public renderAvatar() { public renderAvatar(): JSX.Element | null {
const { const {
avatarPath, avatarPath,
color, color,
@ -32,7 +32,7 @@ export class TypingBubble extends React.PureComponent<Props> {
} = this.props; } = this.props;
if (conversationType !== 'group') { if (conversationType !== 'group') {
return; return null;
} }
return ( return (
@ -52,7 +52,7 @@ export class TypingBubble extends React.PureComponent<Props> {
); );
} }
public render() { public render(): JSX.Element {
const { i18n, color, conversationType } = this.props; const { i18n, color, conversationType } = this.props;
const isGroup = conversationType === 'group'; const isGroup = conversationType === 'group';

View file

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

View file

@ -29,9 +29,12 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping & PropsActions; export type Props = PropsData & PropsHousekeeping & PropsActions;
export class UnsupportedMessage extends React.Component<Props> { export const UnsupportedMessage = ({
public render() { canProcessNow,
const { canProcessNow, contact, i18n, downloadNewVersion } = this.props; contact,
i18n,
downloadNewVersion,
}: Props): JSX.Element => {
const { isMe } = contact; const { isMe } = contact;
const otherStringId = canProcessNow const otherStringId = canProcessNow
@ -47,9 +50,7 @@ export class UnsupportedMessage extends React.Component<Props> {
<div <div
className={classNames( className={classNames(
'module-unsupported-message__icon', 'module-unsupported-message__icon',
canProcessNow canProcessNow ? 'module-unsupported-message__icon--can-process' : null
? 'module-unsupported-message__icon--can-process'
: null
)} )}
/> />
<div className="module-unsupported-message__text"> <div className="module-unsupported-message__text">
@ -75,6 +76,7 @@ export class UnsupportedMessage extends React.Component<Props> {
</div> </div>
{canProcessNow ? null : ( {canProcessNow ? null : (
<button <button
type="button"
onClick={() => { onClick={() => {
downloadNewVersion(); downloadNewVersion();
}} }}
@ -85,5 +87,4 @@ export class UnsupportedMessage extends React.Component<Props> {
)} )}
</div> </div>
); );
} };
}

View file

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

View file

@ -27,7 +27,7 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping; export type Props = PropsData & PropsHousekeeping;
export class VerificationNotification extends React.Component<Props> { export class VerificationNotification extends React.Component<Props> {
public getStringId() { public getStringId(): string {
const { isLocal, type } = this.props; const { isLocal, type } = this.props;
switch (type) { switch (type) {
@ -44,7 +44,7 @@ export class VerificationNotification extends React.Component<Props> {
} }
} }
public renderContents() { public renderContents(): JSX.Element {
const { contact, i18n } = this.props; const { contact, i18n } = this.props;
const id = this.getStringId(); const id = this.getStringId();
@ -67,7 +67,7 @@ export class VerificationNotification extends React.Component<Props> {
); );
} }
public render() { public render(): JSX.Element {
const { type } = this.props; const { type } = this.props;
const suffix = const suffix =
type === 'markVerified' ? 'mark-verified' : 'mark-not-verified'; type === 'markVerified' ? 'mark-verified' : 'mark-not-verified';

View file

@ -19,7 +19,7 @@ export function renderAvatar({
i18n: LocalizerType; i18n: LocalizerType;
size: 28 | 52 | 80; size: 28 | 52 | 80;
direction?: 'outgoing' | 'incoming'; direction?: 'outgoing' | 'incoming';
}) { }): JSX.Element {
const { avatar } = contact; const { avatar } = contact;
const avatarPath = avatar && avatar.avatar && avatar.avatar.path; const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
@ -60,7 +60,7 @@ export function renderName({
contact: ContactType; contact: ContactType;
isIncoming: boolean; isIncoming: boolean;
module: string; module: string;
}) { }): JSX.Element {
return ( return (
<div <div
className={classNames( className={classNames(
@ -81,7 +81,7 @@ export function renderContactShorthand({
contact: ContactType; contact: ContactType;
isIncoming: boolean; isIncoming: boolean;
module: string; module: string;
}) { }): JSX.Element {
const { number: phoneNumber, email } = contact; const { number: phoneNumber, email } = contact;
const firstNumber = phoneNumber && phoneNumber[0] && phoneNumber[0].value; const firstNumber = phoneNumber && phoneNumber[0] && phoneNumber[0].value;
const firstEmail = email && email[0] && email[0].value; const firstEmail = email && email[0] && email[0].value;

View file

@ -1,13 +1,12 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { select, text, withKnobs } from '@storybook/addon-knobs'; import { select, text, withKnobs } from '@storybook/addon-knobs';
import { random, range, sample, sortBy } from 'lodash'; import { random, range, sample, sortBy } from 'lodash';
// @ts-ignore
import { setup as setupI18n } from '../../../../js/modules/i18n'; import { setup as setupI18n } from '../../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../../_locales/en/messages.json'; import enMessages from '../../../../_locales/en/messages.json';
import { MIMEType } from '../../../types/MIME'; import { MIMEType } from '../../../types/MIME';
import { MediaItemType } from '../../LightboxGallery'; import { MediaItemType } from '../../LightboxGallery';
@ -20,6 +19,7 @@ const story = storiesOf(
module module
); );
// eslint-disable-next-line @typescript-eslint/no-explicit-any
story.addDecorator((withKnobs as any)({ escapeHTML: false })); story.addDecorator((withKnobs as any)({ escapeHTML: false }));
export const now = Date.now(); export const now = Date.now();

View file

@ -16,7 +16,7 @@ export interface Props {
} }
export class AttachmentSection extends React.Component<Props> { export class AttachmentSection extends React.Component<Props> {
public render() { public render(): JSX.Element {
const { header } = this.props; const { header } = this.props;
return ( return (

View file

@ -10,6 +10,7 @@ const story = storiesOf(
module module
); );
// eslint-disable-next-line @typescript-eslint/no-explicit-any
story.addDecorator((withKnobs as any)({ escapeHTML: false })); story.addDecorator((withKnobs as any)({ escapeHTML: false }));
story.add('Single', () => ( story.add('Single', () => (

View file

@ -2,7 +2,6 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import moment from 'moment'; import moment from 'moment';
// tslint:disable-next-line:match-default-export-name
import formatFileSize from 'filesize'; import formatFileSize from 'filesize';
interface Props { interface Props {
@ -21,7 +20,7 @@ export class DocumentListItem extends React.Component<Props> {
shouldShowSeparator: true, shouldShowSeparator: true,
}; };
public render() { public render(): JSX.Element {
const { shouldShowSeparator } = this.props; const { shouldShowSeparator } = this.props;
return ( return (
@ -39,12 +38,13 @@ export class DocumentListItem extends React.Component<Props> {
} }
private renderContent() { private renderContent() {
const { fileName, fileSize, timestamp } = this.props; const { fileName, fileSize, onClick, timestamp } = this.props;
return ( return (
<button <button
type="button"
className="module-document-list-item__content" className="module-document-list-item__content"
onClick={this.props.onClick} onClick={onClick}
> >
<div className="module-document-list-item__icon" /> <div className="module-document-list-item__icon" />
<div className="module-document-list-item__metadata"> <div className="module-document-list-item__metadata">

View file

@ -8,6 +8,7 @@ const story = storiesOf(
module module
); );
// eslint-disable-next-line @typescript-eslint/no-explicit-any
story.addDecorator((withKnobs as any)({ escapeHTML: false })); story.addDecorator((withKnobs as any)({ escapeHTML: false }));
story.add('Default', () => { story.add('Default', () => {

View file

@ -1,16 +1,9 @@
/**
* @prettier
*/
import React from 'react'; import React from 'react';
interface Props { interface Props {
label: string; label: string;
} }
export class EmptyState extends React.Component<Props> { export const EmptyState = ({ label }: Props): JSX.Element => (
public render() { <div className="module-empty-state">{label}</div>
const { label } = this.props; );
return <div className="module-empty-state">{label}</div>;
}
}

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
export const LoadingIndicator = () => { export const LoadingIndicator = (): JSX.Element => {
return ( return (
<div className="loading-widget"> <div className="loading-widget">
<div className="container"> <div className="container">

View file

@ -2,9 +2,7 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../../../js/modules/i18n'; 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 { import {

View file

@ -48,6 +48,8 @@ const Tab = ({
: undefined; : undefined;
return ( return (
// Has key events handled elsewhere
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div <div
className={classNames( className={classNames(
'module-media-gallery__tab', 'module-media-gallery__tab',
@ -64,11 +66,15 @@ const Tab = ({
export class MediaGallery extends React.Component<Props, State> { export class MediaGallery extends React.Component<Props, State> {
public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef(); public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();
public state: State = {
constructor(props: Props) {
super(props);
this.state = {
selectedTab: 'media', selectedTab: 'media',
}; };
}
public componentDidMount() { public componentDidMount(): void {
// When this component is created, it's initially not part of the DOM, and then it's // When this component is created, it's initially not part of the DOM, and then it's
// added off-screen and animated in. This ensures that the focus takes. // added off-screen and animated in. This ensures that the focus takes.
setTimeout(() => { setTimeout(() => {
@ -78,7 +84,7 @@ export class MediaGallery extends React.Component<Props, State> {
}); });
} }
public render() { public render(): JSX.Element {
const { selectedTab } = this.state; const { selectedTab } = this.state;
return ( return (

View file

@ -3,11 +3,8 @@ import { storiesOf } from '@storybook/react';
import { text, withKnobs } from '@storybook/addon-knobs'; import { text, withKnobs } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../../../js/modules/i18n'; import { setup as setupI18n } from '../../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../../_locales/en/messages.json'; import enMessages from '../../../../_locales/en/messages.json';
import { MediaItemType } from '../../LightboxGallery'; import { MediaItemType } from '../../LightboxGallery';
import { AttachmentType } from '../../../types/Attachment'; import { AttachmentType } from '../../../types/Attachment';
import { MIMEType } from '../../../types/MIME'; import { MIMEType } from '../../../types/MIME';
@ -22,6 +19,7 @@ const story = storiesOf(
module module
); );
// eslint-disable-next-line @typescript-eslint/no-explicit-any
story.addDecorator((withKnobs as any)({ escapeHTML: false })); story.addDecorator((withKnobs as any)({ escapeHTML: false }));
const createProps = ( const createProps = (

View file

@ -31,9 +31,8 @@ export class MediaGridItem extends React.Component<Props, State> {
this.onImageErrorBound = this.onImageError.bind(this); this.onImageErrorBound = this.onImageError.bind(this);
} }
public onImageError() { public onImageError(): void {
// tslint:disable-next-line no-console window.log.info(
console.log(
'MediaGridItem: Image failed to load; failing over to placeholder' 'MediaGridItem: Image failed to load; failing over to placeholder'
); );
this.setState({ this.setState({
@ -41,7 +40,7 @@ export class MediaGridItem extends React.Component<Props, State> {
}); });
} }
public renderContent() { public renderContent(): JSX.Element | null {
const { mediaItem, i18n } = this.props; const { mediaItem, i18n } = this.props;
const { imageBroken } = this.state; const { imageBroken } = this.state;
const { attachment, contentType } = mediaItem; const { attachment, contentType } = mediaItem;
@ -70,7 +69,8 @@ export class MediaGridItem extends React.Component<Props, State> {
onError={this.onImageErrorBound} onError={this.onImageErrorBound}
/> />
); );
} else if (contentType && isVideoTypeSupported(contentType)) { }
if (contentType && isVideoTypeSupported(contentType)) {
if (imageBroken || !mediaItem.thumbnailObjectUrl) { if (imageBroken || !mediaItem.thumbnailObjectUrl) {
return ( return (
<div <div
@ -107,9 +107,15 @@ export class MediaGridItem extends React.Component<Props, State> {
); );
} }
public render() { public render(): JSX.Element {
const { onClick } = this.props;
return ( return (
<button className="module-media-grid-item" onClick={this.props.onClick}> <button
type="button"
className="module-media-grid-item"
onClick={onClick}
>
{this.renderContent()} {this.renderContent()}
</button> </button>
); );

View file

@ -67,11 +67,13 @@ const toSection = (
case 'yesterday': case 'yesterday':
case 'thisWeek': case 'thisWeek':
case 'thisMonth': case 'thisMonth':
// eslint-disable-next-line consistent-return
return { return {
type: firstMediaItemWithSection.type, type: firstMediaItemWithSection.type,
mediaItems, mediaItems,
}; };
case 'yearMonth': case 'yearMonth':
// eslint-disable-next-line consistent-return
return { return {
type: firstMediaItemWithSection.type, type: firstMediaItemWithSection.type,
year: firstMediaItemWithSection.year, year: firstMediaItemWithSection.year,
@ -83,6 +85,7 @@ const toSection = (
// error TS2345: Argument of type 'any' is not assignable to parameter // error TS2345: Argument of type 'any' is not assignable to parameter
// of type 'never'. // of type 'never'.
// return missingCaseError(firstMediaItemWithSection.type); // return missingCaseError(firstMediaItemWithSection.type);
// eslint-disable-next-line no-useless-return
return; return;
} }
}; };

View file

@ -3,5 +3,7 @@ import { Attachment } from '../../../../types/Attachment';
export type Message = { export type Message = {
id: string; id: string;
attachments: Array<Attachment>; attachments: Array<Attachment>;
// Assuming this is for the API
// eslint-disable-next-line camelcase
received_at: number; received_at: number;
}; };

View file

@ -12830,18 +12830,17 @@
"path": "ts/components/CallScreen.js", "path": "ts/components/CallScreen.js",
"line": " this.localVideoRef = react_1.default.createRef();", "line": " this.localVideoRef = react_1.default.createRef();",
"lineNumber": 98, "lineNumber": 98,
"reasonCategory": "usageTrusted", "reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2020-05-28T17:22:06.472Z", "updated": "2020-09-14T23:03:44.863Z",
"reasonDetail": "Used to render local preview video" "reasonDetail": "<optional>"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/CallScreen.js", "path": "ts/components/CallScreen.js",
"line": " this.remoteVideoRef = react_1.default.createRef();", "line": " this.remoteVideoRef = react_1.default.createRef();",
"lineNumber": 98, "lineNumber": 99,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-11T17:24:56.124Z", "updated": "2020-09-14T23:03:44.863Z"
"reasonDetail": "Necessary for showing call video"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",
@ -12856,10 +12855,9 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/CallScreen.tsx", "path": "ts/components/CallScreen.tsx",
"line": " this.remoteVideoRef = React.createRef();", "line": " this.remoteVideoRef = React.createRef();",
"lineNumber": 75, "lineNumber": 80,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-11T17:24:56.124Z", "updated": "2020-09-14T23:03:44.863Z"
"reasonDetail": "Necessary for showing call video"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",
@ -12944,10 +12942,9 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/Lightbox.js", "path": "ts/components/Lightbox.js",
"line": " this.videoRef = react_1.default.createRef();", "line": " this.videoRef = react_1.default.createRef();",
"lineNumber": 142, "lineNumber": 149,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-11T17:24:56.124Z", "updated": "2020-09-14T23:03:44.863Z"
"reasonDetail": "Used to control video"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",
@ -13016,7 +13013,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.tsx", "path": "ts/components/conversation/ConversationHeader.tsx",
"line": " this.menuTriggerRef = React.createRef();", "line": " this.menuTriggerRef = React.createRef();",
"lineNumber": 79, "lineNumber": 82,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-20T20:10:43.540Z", "updated": "2020-05-20T20:10:43.540Z",
"reasonDetail": "Used to reference popup menu" "reasonDetail": "Used to reference popup menu"
@ -13069,24 +13066,23 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();", "line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
"lineNumber": 212, "lineNumber": 213,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-08-28T19:36:40.817Z" "updated": "2020-09-14T23:03:44.863Z"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();", "line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 213, "lineNumber": 215,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-11T17:24:56.124Z", "updated": "2020-09-14T23:03:44.863Z"
"reasonDetail": "Used for managing focus only"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " > = React.createRef();", "line": " > = React.createRef();",
"lineNumber": 216, "lineNumber": 219,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-08-28T19:36:40.817Z" "updated": "2020-08-28T19:36:40.817Z"
}, },
@ -13094,7 +13090,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/MessageDetail.js", "path": "ts/components/conversation/MessageDetail.js",
"line": " this.focusRef = react_1.default.createRef();", "line": " this.focusRef = react_1.default.createRef();",
"lineNumber": 15, "lineNumber": 18,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-11-01T22:46:33.013Z", "updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only" "reasonDetail": "Used for setting focus only"
@ -13112,7 +13108,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/media-gallery/MediaGallery.js", "path": "ts/components/conversation/media-gallery/MediaGallery.js",
"line": " this.focusRef = react_1.default.createRef();", "line": " this.focusRef = react_1.default.createRef();",
"lineNumber": 25, "lineNumber": 28,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-11-01T22:46:33.013Z", "updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only" "reasonDetail": "Used for setting focus only"
@ -13121,7 +13117,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/media-gallery/MediaGallery.tsx", "path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
"line": " public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();", "line": " public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 66, "lineNumber": 68,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-11-01T22:46:33.013Z", "updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only" "reasonDetail": "Used for setting focus only"

View file

@ -181,6 +181,7 @@
"ts/backbone/**", "ts/backbone/**",
"ts/build/**", "ts/build/**",
"ts/components/*.ts[x]", "ts/components/*.ts[x]",
"ts/components/conversation/**",
"ts/components/emoji/**", "ts/components/emoji/**",
"ts/notifications/**", "ts/notifications/**",
"ts/protobuf/**", "ts/protobuf/**",