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/**/*.tsx
ts/*.ts
ts/components/conversation/**
ts/components/*.ts
ts/components/*.tsx
ts/components/stickers/**
ts/shims/**
ts/sql/**

View File

@ -118,6 +118,8 @@ module.exports = {
rules: {
...rules,
'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",
"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": {
"message": "Copy Link",
"description": "Shown in the context menu for a link to indicate that the user can copy the link"
@ -985,6 +989,10 @@
"theirIdentityUnknown": {
"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": {
"message": "More Info...",
"description": "Shown on the drop-down menu for an individual message, takes you to message detail screen"
@ -2772,6 +2780,14 @@
"message": "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": {
"message": "Reconnecting...",
"description": "Shown in the call screen when the call is reconnecting due to network issues"
@ -3574,7 +3590,7 @@
}
},
"close": {
"message": "close",
"message": "Close",
"description": "Generic close label"
},
"previous": {

View File

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

View File

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

View File

@ -27,89 +27,81 @@ export interface Props {
const IMAGE_WIDTH = 120;
const IMAGE_HEIGHT = 120;
export class AttachmentList extends React.Component<Props> {
// tslint:disable-next-line max-func-body-length */
public render() {
const {
attachments,
i18n,
onAddAttachment,
onClickAttachment,
onCloseAttachment,
onClose,
} = this.props;
export const AttachmentList = ({
attachments,
i18n,
onAddAttachment,
onClickAttachment,
onCloseAttachment,
onClose,
}: Props): JSX.Element | null => {
if (!attachments.length) {
return null;
}
if (!attachments.length) {
return null;
}
const allVisualAttachments = areAllAttachmentsVisual(attachments);
const allVisualAttachments = areAllAttachmentsVisual(attachments);
return (
<div className="module-attachments">
{attachments.length > 1 ? (
<div className="module-attachments__header">
<button
onClick={onClose}
className="module-attachments__close-button"
/>
</div>
) : null}
<div className="module-attachments__rail">
{(attachments || []).map((attachment, index) => {
const { contentType } = attachment;
if (
isImageTypeSupported(contentType) ||
isVideoTypeSupported(contentType)
) {
const imageKey =
getUrl(attachment) || attachment.fileName || index;
const clickCallback =
attachments.length > 1 ? onClickAttachment : undefined;
return (
<Image
key={imageKey}
alt={i18n('stagedImageAttachment', [
getUrl(attachment) || attachment.fileName,
])}
i18n={i18n}
attachment={attachment}
softCorners={true}
playIconOverlay={isVideoAttachment(attachment)}
height={IMAGE_HEIGHT}
width={IMAGE_WIDTH}
url={getUrl(attachment)}
closeButton={true}
onClick={clickCallback}
onClickClose={onCloseAttachment}
onError={() => {
onCloseAttachment(attachment);
}}
/>
);
}
const genericKey =
getUrl(attachment) || attachment.fileName || index;
return (
<div className="module-attachments">
{attachments.length > 1 ? (
<div className="module-attachments__header">
<button
type="button"
onClick={onClose}
className="module-attachments__close-button"
aria-label={i18n('close')}
/>
</div>
) : null}
<div className="module-attachments__rail">
{(attachments || []).map((attachment, index) => {
const { contentType } = attachment;
if (
isImageTypeSupported(contentType) ||
isVideoTypeSupported(contentType)
) {
const imageKey = getUrl(attachment) || attachment.fileName || index;
const clickCallback =
attachments.length > 1 ? onClickAttachment : undefined;
return (
<StagedGenericAttachment
key={genericKey}
attachment={attachment}
<Image
key={imageKey}
alt={i18n('stagedImageAttachment', [
getUrl(attachment) || attachment.fileName,
])}
i18n={i18n}
onClose={onCloseAttachment}
attachment={attachment}
softCorners
playIconOverlay={isVideoAttachment(attachment)}
height={IMAGE_HEIGHT}
width={IMAGE_WIDTH}
url={getUrl(attachment)}
closeButton
onClick={clickCallback}
onClickClose={onCloseAttachment}
onError={() => {
onCloseAttachment(attachment);
}}
/>
);
})}
{allVisualAttachments ? (
<StagedPlaceholderAttachment
onClick={onAddAttachment}
}
const genericKey = getUrl(attachment) || attachment.fileName || index;
return (
<StagedGenericAttachment
key={genericKey}
attachment={attachment}
i18n={i18n}
onClose={onCloseAttachment}
/>
) : null}
</div>
);
})}
{allVisualAttachments ? (
<StagedPlaceholderAttachment onClick={onAddAttachment} i18n={i18n} />
) : null}
</div>
);
}
}
</div>
);
};

View File

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

View File

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

View File

@ -72,6 +72,7 @@ function getLabelForAddress(
}
export class ContactDetail extends React.Component<Props> {
// eslint-disable-next-line class-methods-use-this
public renderSendMessage({
hasSignalAccount,
i18n,
@ -80,20 +81,24 @@ export class ContactDetail extends React.Component<Props> {
hasSignalAccount: boolean;
i18n: (key: string, values?: Array<string>) => string;
onSendMessage: () => void;
}) {
}): JSX.Element | null {
if (!hasSignalAccount) {
return null;
}
// 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.
const onClick = (e: React.MouseEvent<{}>): void => {
const onClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
e.stopPropagation();
onSendMessage();
};
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__bubble-icon" />
{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) {
return;
return undefined;
}
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) {
return;
return undefined;
}
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) {
return;
return undefined;
}
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) {
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) {
return (
<div>
@ -177,13 +196,14 @@ export class ContactDetail extends React.Component<Props> {
public renderAddresses(
addresses: Array<PostalAddress> | undefined,
i18n: LocalizerType
) {
): Array<JSX.Element> | undefined {
if (!addresses || addresses.length === 0) {
return;
return undefined;
}
return addresses.map((address: PostalAddress, index: number) => {
return (
// eslint-disable-next-line react/no-array-index-key
<div key={index} className="module-contact-detail__additional-contact">
<div className="module-contact-detail__additional-contact__type">
{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 isIncoming = false;
const module = 'contact-detail';

View File

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

View File

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

View File

@ -3,18 +3,14 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../\_locales/en/messages.json';
import enMessages from '../../../_locales/en/messages.json';
import {
ConversationHeader,
PropsActionsType,
PropsHousekeepingType,
PropsType,
} from './ConversationHeader';
import { gifUrl } from '../../storybook/Fixtures';
const book = storiesOf('Components/Conversation/ConversationHeader', module);

View File

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

View File

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

View File

@ -35,6 +35,8 @@ const renderMembershipRow = ({
sharedGroupNames.length > 0
) {
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}>
<Emojify text={group} />
</strong>
@ -56,7 +58,8 @@ const renderMembershipRow = ({
/>
</div>
);
} else if (firstThreeGroups.length === 3) {
}
if (firstThreeGroups.length === 3) {
return (
<div className={className}>
<Intl
@ -70,7 +73,8 @@ const renderMembershipRow = ({
/>
</div>
);
} else if (firstThreeGroups.length >= 2) {
}
if (firstThreeGroups.length >= 2) {
return (
<div className={className}>
<Intl
@ -83,7 +87,8 @@ const renderMembershipRow = ({
/>
</div>
);
} else if (firstThreeGroups.length >= 1) {
}
if (firstThreeGroups.length >= 1) {
return (
<div className={className}>
<Intl
@ -115,9 +120,11 @@ export const ConversationHero = ({
title,
onHeightChange,
updateSharedGroups,
}: Props) => {
}: Props): JSX.Element => {
const firstRenderRef = React.useRef(true);
// TODO: DESKTOP-686
/* eslint-disable react-hooks/exhaustive-deps */
React.useEffect(() => {
// If any of the depenencies for this hook change then the height of this
// component may have changed. The cleanup function notifies listeners of
@ -144,11 +151,13 @@ export const ConversationHero = ({
`pn-${profileName}`,
sharedGroupNames.map(g => `g-${g}`).join(' '),
]);
/* eslint-enable react-hooks/exhaustive-deps */
const phoneNumberOnly = Boolean(
!name && !profileName && conversationType === 'direct'
);
/* eslint-disable no-nested-ternary */
return (
<div className="module-conversation-hero">
<Avatar
@ -190,4 +199,5 @@ export const ConversationHero = ({
{renderMembershipRow({ isMe, sharedGroupNames, conversationType, i18n })}
</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 { EmbeddedContact, Props } from './EmbeddedContact';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json';
import { ContactFormType } from '../../types/Contact';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/EmbeddedContact', module);

View File

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

View File

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

View File

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

View File

@ -33,7 +33,10 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping;
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 { i18n } = this.props;
@ -78,6 +81,7 @@ export class GroupNotification extends React.Component<Props> {
throw new Error('Group update is missing contacts');
}
// eslint-disable-next-line no-case-declarations
const otherPeopleNotifMsg =
otherPeople.length === 1
? 'joinedTheGroup'
@ -108,6 +112,7 @@ export class GroupNotification extends React.Component<Props> {
throw new Error('Group update is missing contacts');
}
// eslint-disable-next-line no-case-declarations
const leftKey =
contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
@ -115,13 +120,14 @@ export class GroupNotification extends React.Component<Props> {
<Intl i18n={i18n} id={leftKey} components={[otherPeopleWithCommas]} />
);
case 'general':
// eslint-disable-next-line consistent-return
return;
default:
throw missingCaseError(type);
}
}
public render() {
public render(): JSX.Element {
const { changes, i18n, from } = this.props;
// 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 />
</>
)}
{(changes || []).map((change, index) => (
<div key={index} className="module-group-notification__change">
{(changes || []).map((change, i) => (
// eslint-disable-next-line react/no-array-index-key
<div key={i} className="module-group-notification__change">
{this.renderChange(change, from)}
</div>
))}

View File

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

View File

@ -53,6 +53,8 @@ export function GroupV2Change(props: PropsType): React.ReactElement {
renderString: renderStringToIntl,
RoleEnum,
}).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>

View File

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

View File

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

View File

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

View File

@ -30,241 +30,162 @@ export interface Props {
onClick?: (attachment: AttachmentType) => void;
}
export class ImageGrid extends React.Component<Props> {
// tslint:disable-next-line max-func-body-length */
public render() {
const {
attachments,
bottomOverlay,
i18n,
isSticker,
stickerSize,
onError,
onClick,
tabIndex,
withContentAbove,
withContentBelow,
} = this.props;
export const ImageGrid = ({
attachments,
bottomOverlay,
i18n,
isSticker,
stickerSize,
onError,
onClick,
tabIndex,
withContentAbove,
withContentBelow,
}: Props): JSX.Element | null => {
const curveTopLeft = !withContentAbove;
const curveTopRight = curveTopLeft;
const curveTopLeft = !Boolean(withContentAbove);
const curveTopRight = curveTopLeft;
const curveBottom = !withContentBelow;
const curveBottomLeft = curveBottom;
const curveBottomRight = curveBottom;
const curveBottom = !Boolean(withContentBelow);
const curveBottomLeft = curveBottom;
const curveBottomRight = curveBottom;
const withBottomOverlay = Boolean(bottomOverlay && curveBottom);
const withBottomOverlay = Boolean(bottomOverlay && curveBottom);
if (!attachments || !attachments.length) {
return null;
}
if (!attachments || !attachments.length) {
return null;
}
if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) {
const { height, width } = getImageDimensions(
attachments[0],
isSticker ? stickerSize : undefined
);
if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) {
const { height, width } = getImageDimensions(
attachments[0],
isSticker ? stickerSize : undefined
);
return (
<div
className={classNames(
'module-image-grid',
'module-image-grid--one-image',
isSticker ? 'module-image-grid--with-sticker' : null
)}
>
<Image
alt={getAlt(attachments[0], i18n)}
i18n={i18n}
blurHash={attachments[0].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={isSticker}
noBackground={isSticker}
curveTopLeft={curveTopLeft}
curveTopRight={curveTopRight}
curveBottomLeft={curveBottomLeft}
curveBottomRight={curveBottomRight}
attachment={attachments[0]}
playIconOverlay={isVideoAttachment(attachments[0])}
height={height}
width={width}
url={getUrl(attachments[0])}
tabIndex={tabIndex}
onClick={onClick}
onError={onError}
/>
</div>
);
}
return (
<div
className={classNames(
'module-image-grid',
'module-image-grid--one-image',
isSticker ? 'module-image-grid--with-sticker' : null
)}
>
<Image
alt={getAlt(attachments[0], i18n)}
i18n={i18n}
blurHash={attachments[0].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={isSticker}
noBackground={isSticker}
curveTopLeft={curveTopLeft}
curveTopRight={curveTopRight}
curveBottomLeft={curveBottomLeft}
curveBottomRight={curveBottomRight}
attachment={attachments[0]}
playIconOverlay={isVideoAttachment(attachments[0])}
height={height}
width={width}
url={getUrl(attachments[0])}
tabIndex={tabIndex}
onClick={onClick}
onError={onError}
/>
</div>
);
}
if (attachments.length === 2) {
return (
<div className="module-image-grid">
<Image
alt={getAlt(attachments[0], i18n)}
i18n={i18n}
attachment={attachments[0]}
blurHash={attachments[0].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={false}
curveTopLeft={curveTopLeft}
curveBottomLeft={curveBottomLeft}
playIconOverlay={isVideoAttachment(attachments[0])}
height={149}
width={149}
url={getThumbnailUrl(attachments[0])}
onClick={onClick}
onError={onError}
/>
<Image
alt={getAlt(attachments[1], i18n)}
i18n={i18n}
blurHash={attachments[1].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={false}
curveTopRight={curveTopRight}
curveBottomRight={curveBottomRight}
playIconOverlay={isVideoAttachment(attachments[1])}
height={149}
width={149}
attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])}
onClick={onClick}
onError={onError}
/>
</div>
);
}
if (attachments.length === 2) {
return (
<div className="module-image-grid">
<Image
alt={getAlt(attachments[0], i18n)}
i18n={i18n}
attachment={attachments[0]}
blurHash={attachments[0].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={false}
curveTopLeft={curveTopLeft}
curveBottomLeft={curveBottomLeft}
playIconOverlay={isVideoAttachment(attachments[0])}
height={149}
width={149}
url={getThumbnailUrl(attachments[0])}
onClick={onClick}
onError={onError}
/>
if (attachments.length === 3) {
return (
<div className="module-image-grid">
<Image
alt={getAlt(attachments[0], i18n)}
i18n={i18n}
blurHash={attachments[0].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={false}
curveTopLeft={curveTopLeft}
curveBottomLeft={curveBottomLeft}
attachment={attachments[0]}
playIconOverlay={isVideoAttachment(attachments[0])}
height={200}
width={199}
url={getUrl(attachments[0])}
onClick={onClick}
onError={onError}
/>
<div className="module-image-grid__column">
<Image
alt={getAlt(attachments[1], i18n)}
i18n={i18n}
blurHash={attachments[1].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={false}
curveTopRight={curveTopRight}
curveBottomRight={curveBottomRight}
playIconOverlay={isVideoAttachment(attachments[1])}
height={149}
width={149}
height={99}
width={99}
attachment={attachments[1]}
playIconOverlay={isVideoAttachment(attachments[1])}
url={getThumbnailUrl(attachments[1])}
onClick={onClick}
onError={onError}
/>
</div>
);
}
if (attachments.length === 3) {
return (
<div className="module-image-grid">
<Image
alt={getAlt(attachments[0], i18n)}
alt={getAlt(attachments[2], i18n)}
i18n={i18n}
blurHash={attachments[0].blurHash}
blurHash={attachments[2].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={false}
curveTopLeft={curveTopLeft}
curveBottomLeft={curveBottomLeft}
attachment={attachments[0]}
playIconOverlay={isVideoAttachment(attachments[0])}
height={200}
width={199}
url={getUrl(attachments[0])}
curveBottomRight={curveBottomRight}
height={99}
width={99}
attachment={attachments[2]}
playIconOverlay={isVideoAttachment(attachments[2])}
url={getThumbnailUrl(attachments[2])}
onClick={onClick}
onError={onError}
/>
<div className="module-image-grid__column">
<Image
alt={getAlt(attachments[1], i18n)}
i18n={i18n}
blurHash={attachments[1].blurHash}
curveTopRight={curveTopRight}
height={99}
width={99}
attachment={attachments[1]}
playIconOverlay={isVideoAttachment(attachments[1])}
url={getThumbnailUrl(attachments[1])}
onClick={onClick}
onError={onError}
/>
<Image
alt={getAlt(attachments[2], i18n)}
i18n={i18n}
blurHash={attachments[2].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={false}
curveBottomRight={curveBottomRight}
height={99}
width={99}
attachment={attachments[2]}
playIconOverlay={isVideoAttachment(attachments[2])}
url={getThumbnailUrl(attachments[2])}
onClick={onClick}
onError={onError}
/>
</div>
</div>
);
}
if (attachments.length === 4) {
return (
<div className="module-image-grid">
<div className="module-image-grid__column">
<div className="module-image-grid__row">
<Image
alt={getAlt(attachments[0], i18n)}
i18n={i18n}
blurHash={attachments[0].blurHash}
curveTopLeft={curveTopLeft}
noBorder={false}
attachment={attachments[0]}
playIconOverlay={isVideoAttachment(attachments[0])}
height={149}
width={149}
url={getThumbnailUrl(attachments[0])}
onClick={onClick}
onError={onError}
/>
<Image
alt={getAlt(attachments[1], i18n)}
i18n={i18n}
blurHash={attachments[1].blurHash}
curveTopRight={curveTopRight}
playIconOverlay={isVideoAttachment(attachments[1])}
noBorder={false}
height={149}
width={149}
attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])}
onClick={onClick}
onError={onError}
/>
</div>
<div className="module-image-grid__row">
<Image
alt={getAlt(attachments[2], i18n)}
i18n={i18n}
blurHash={attachments[2].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={false}
curveBottomLeft={curveBottomLeft}
playIconOverlay={isVideoAttachment(attachments[2])}
height={149}
width={149}
attachment={attachments[2]}
url={getThumbnailUrl(attachments[2])}
onClick={onClick}
onError={onError}
/>
<Image
alt={getAlt(attachments[3], i18n)}
i18n={i18n}
blurHash={attachments[3].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={false}
curveBottomRight={curveBottomRight}
playIconOverlay={isVideoAttachment(attachments[3])}
height={149}
width={149}
attachment={attachments[3]}
url={getThumbnailUrl(attachments[3])}
onClick={onClick}
onError={onError}
/>
</div>
</div>
</div>
);
}
const moreMessagesOverlay = attachments.length > 5;
const moreMessagesOverlayText = moreMessagesOverlay
? `+${attachments.length - 5}`
: undefined;
</div>
);
}
if (attachments.length === 4) {
return (
<div className="module-image-grid">
<div className="module-image-grid__column">
@ -274,6 +195,7 @@ export class ImageGrid extends React.Component<Props> {
i18n={i18n}
blurHash={attachments[0].blurHash}
curveTopLeft={curveTopLeft}
noBorder={false}
attachment={attachments[0]}
playIconOverlay={isVideoAttachment(attachments[0])}
height={149}
@ -288,6 +210,7 @@ export class ImageGrid extends React.Component<Props> {
blurHash={attachments[1].blurHash}
curveTopRight={curveTopRight}
playIconOverlay={isVideoAttachment(attachments[1])}
noBorder={false}
height={149}
width={149}
attachment={attachments[1]}
@ -302,11 +225,11 @@ export class ImageGrid extends React.Component<Props> {
i18n={i18n}
blurHash={attachments[2].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={isSticker}
noBorder={false}
curveBottomLeft={curveBottomLeft}
playIconOverlay={isVideoAttachment(attachments[2])}
height={99}
width={99}
height={149}
width={149}
attachment={attachments[2]}
url={getThumbnailUrl(attachments[2])}
onClick={onClick}
@ -317,35 +240,107 @@ export class ImageGrid extends React.Component<Props> {
i18n={i18n}
blurHash={attachments[3].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={isSticker}
noBorder={false}
curveBottomRight={curveBottomRight}
playIconOverlay={isVideoAttachment(attachments[3])}
height={99}
width={98}
height={149}
width={149}
attachment={attachments[3]}
url={getThumbnailUrl(attachments[3])}
onClick={onClick}
onError={onError}
/>
<Image
alt={getAlt(attachments[4], i18n)}
i18n={i18n}
blurHash={attachments[4].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={isSticker}
curveBottomRight={curveBottomRight}
playIconOverlay={isVideoAttachment(attachments[4])}
height={99}
width={99}
darkOverlay={moreMessagesOverlay}
overlayText={moreMessagesOverlayText}
attachment={attachments[4]}
url={getThumbnailUrl(attachments[4])}
onClick={onClick}
onError={onError}
/>
</div>
</div>
</div>
);
}
}
const moreMessagesOverlay = attachments.length > 5;
const moreMessagesOverlayText = moreMessagesOverlay
? `+${attachments.length - 5}`
: undefined;
return (
<div className="module-image-grid">
<div className="module-image-grid__column">
<div className="module-image-grid__row">
<Image
alt={getAlt(attachments[0], i18n)}
i18n={i18n}
blurHash={attachments[0].blurHash}
curveTopLeft={curveTopLeft}
attachment={attachments[0]}
playIconOverlay={isVideoAttachment(attachments[0])}
height={149}
width={149}
url={getThumbnailUrl(attachments[0])}
onClick={onClick}
onError={onError}
/>
<Image
alt={getAlt(attachments[1], i18n)}
i18n={i18n}
blurHash={attachments[1].blurHash}
curveTopRight={curveTopRight}
playIconOverlay={isVideoAttachment(attachments[1])}
height={149}
width={149}
attachment={attachments[1]}
url={getThumbnailUrl(attachments[1])}
onClick={onClick}
onError={onError}
/>
</div>
<div className="module-image-grid__row">
<Image
alt={getAlt(attachments[2], i18n)}
i18n={i18n}
blurHash={attachments[2].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={isSticker}
curveBottomLeft={curveBottomLeft}
playIconOverlay={isVideoAttachment(attachments[2])}
height={99}
width={99}
attachment={attachments[2]}
url={getThumbnailUrl(attachments[2])}
onClick={onClick}
onError={onError}
/>
<Image
alt={getAlt(attachments[3], i18n)}
i18n={i18n}
blurHash={attachments[3].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={isSticker}
playIconOverlay={isVideoAttachment(attachments[3])}
height={99}
width={98}
attachment={attachments[3]}
url={getThumbnailUrl(attachments[3])}
onClick={onClick}
onError={onError}
/>
<Image
alt={getAlt(attachments[4], i18n)}
i18n={i18n}
blurHash={attachments[4].blurHash}
bottomOverlay={withBottomOverlay}
noBorder={isSticker}
curveBottomRight={curveBottomRight}
playIconOverlay={isVideoAttachment(attachments[4])}
height={99}
width={99}
darkOverlay={moreMessagesOverlay}
overlayText={moreMessagesOverlayText}
attachment={attachments[4]}
url={getThumbnailUrl(attachments[4])}
onClick={onClick}
onError={onError}
/>
</div>
</div>
</div>
);
};

View File

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

View File

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

View File

@ -7,20 +7,16 @@ export type Props = {
i18n: LocalizerType;
};
export class LastSeenIndicator extends React.Component<Props> {
public render() {
const { count, i18n } = this.props;
export const LastSeenIndicator = ({ count, i18n }: Props): JSX.Element => {
const message =
count === 1
? i18n('unreadMessage')
: i18n('unreadMessages', [String(count)]);
const message =
count === 1
? i18n('unreadMessage')
: i18n('unreadMessages', [String(count)]);
return (
<div className="module-last-seen-indicator">
<div className="module-last-seen-indicator__bar" />
<div className="module-last-seen-indicator__text">{message}</div>
</div>
);
}
}
return (
<div className="module-last-seen-indicator">
<div className="module-last-seen-indicator__bar" />
<div className="module-last-seen-indicator__text">{message}</div>
</div>
);
};

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import ReactDOM, { createPortal } from 'react-dom';
import classNames from 'classnames';
import Measure from 'react-measure';
import { drop, groupBy, orderBy, take } from 'lodash';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
import { Manager, Popper, Reference } from 'react-popper';
import moment, { Moment } from 'moment';
@ -24,6 +25,7 @@ import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker';
import { Emoji } from '../emoji/Emoji';
import {
AttachmentType,
canDisplayImage,
getExtensionForDisplay,
getGridDimensions,
@ -34,8 +36,7 @@ import {
isImage,
isImageAttachment,
isVideo,
} from '../../../ts/types/Attachment';
import { AttachmentType } from '../../types/Attachment';
} from '../../types/Attachment';
import { ContactType } from '../../types/Contact';
import { getIncrement } from '../../util/timer';
@ -43,7 +44,6 @@ import { isFileDangerous } from '../../util/isFileDangerous';
import { BodyRangesType, LocalizerType } from '../../types/Util';
import { ColorType } from '../../types/Colors';
import { createRefMerger } from '../_util';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
interface Trigger {
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
@ -209,18 +209,24 @@ const EXPIRED_DELAY = 600;
export class Message extends React.PureComponent<Props, State> {
public menuTriggerRef: Trigger | undefined;
public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();
public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
public reactionsContainerRef: React.RefObject<
HTMLDivElement
> = React.createRef();
public reactionsContainerRefMerger = createRefMerger();
public wideMl: MediaQueryList;
public expirationCheckInterval: any;
public expiredTimeout: any;
public selectedTimeout: any;
public expirationCheckInterval: NodeJS.Timeout | undefined;
public expiredTimeout: NodeJS.Timeout | undefined;
public selectedTimeout: NodeJS.Timeout | undefined;
public constructor(props: Props) {
super(props);
@ -268,24 +274,23 @@ export class Message extends React.PureComponent<Props, State> {
return state;
}
public handleWideMlChange = (event: MediaQueryListEvent) => {
public handleWideMlChange = (event: MediaQueryListEvent): void => {
this.setState({ isWide: event.matches });
};
public captureMenuTrigger = (triggerRef: Trigger) => {
public captureMenuTrigger = (triggerRef: Trigger): void => {
this.menuTriggerRef = triggerRef;
};
public showMenu = (event: React.MouseEvent<HTMLDivElement>) => {
public showMenu = (event: React.MouseEvent<HTMLDivElement>): void => {
if (this.menuTriggerRef) {
this.menuTriggerRef.handleContextClick(event);
}
};
public handleImageError = () => {
public handleImageError = (): void => {
const { id } = this.props;
// tslint:disable-next-line no-console
console.log(
window.log.info(
`Message ${id}: Image failed to load; failing over to placeholder`
);
this.setState({
@ -293,7 +298,7 @@ export class Message extends React.PureComponent<Props, State> {
});
};
public handleFocus = () => {
public handleFocus = (): void => {
const { interactionMode } = this.props;
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;
if (selectMessage) {
@ -309,7 +314,7 @@ export class Message extends React.PureComponent<Props, State> {
}
};
public setFocus = () => {
public setFocus = (): void => {
const container = this.focusRef.current;
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();
const { isSelected } = this.props;
@ -340,7 +345,7 @@ export class Message extends React.PureComponent<Props, State> {
}, checkFrequency);
}
public componentWillUnmount() {
public componentWillUnmount(): void {
if (this.selectedTimeout) {
clearInterval(this.selectedTimeout);
}
@ -356,18 +361,20 @@ export class Message extends React.PureComponent<Props, State> {
this.wideMl.removeEventListener('change', this.handleWideMlChange);
}
public componentDidUpdate(prevProps: Props) {
public componentDidUpdate(prevProps: Props): void {
const { isSelected } = this.props;
this.startSelectedTimer();
if (!prevProps.isSelected && this.props.isSelected) {
if (!prevProps.isSelected && isSelected) {
this.setFocus();
}
this.checkExpired();
}
public startSelectedTimer() {
const { interactionMode } = this.props;
public startSelectedTimer(): void {
const { clearSelectedMessage, interactionMode } = this.props;
const { isSelected } = this.state;
if (interactionMode === 'keyboard' || !isSelected) {
@ -378,12 +385,12 @@ export class Message extends React.PureComponent<Props, State> {
this.selectedTimeout = setTimeout(() => {
this.selectedTimeout = undefined;
this.setState({ isSelected: false });
this.props.clearSelectedMessage();
clearSelectedMessage();
}, SELECTED_TIMEOUT);
}
}
public checkExpired() {
public checkExpired(): void {
const now = Date.now();
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 {
direction,
i18n,
@ -442,6 +449,7 @@ export class Message extends React.PureComponent<Props, State> {
i18n('sendFailed')
) : (
<button
type="button"
className="module-message__metadata__tapable"
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
@ -463,7 +471,7 @@ export class Message extends React.PureComponent<Props, State> {
<Timestamp
i18n={i18n}
timestamp={timestamp}
extended={true}
extended
direction={metadataDirection}
withImageNoCaption={withImageNoCaption}
withSticker={isSticker}
@ -473,8 +481,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
// tslint:disable-next-line cyclomatic-complexity
public renderMetadata() {
public renderMetadata(): JSX.Element | null {
const {
collapseMetadata,
direction,
@ -548,7 +555,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
public renderAuthor() {
public renderAuthor(): JSX.Element | null {
const {
authorTitle,
authorName,
@ -564,7 +571,7 @@ export class Message extends React.PureComponent<Props, State> {
} = this.props;
if (collapseMetadata) {
return;
return null;
}
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() {
public renderAttachment(): JSX.Element | null {
const {
attachments,
collapseMetadata,
@ -667,11 +673,12 @@ export class Message extends React.PureComponent<Props, State> {
/>
</div>
);
} else if (!firstAttachment.pending && isAudio(attachments)) {
}
if (!firstAttachment.pending && isAudio(attachments)) {
return (
<audio
ref={this.audioRef}
controls={true}
controls
className={classNames(
'module-message__audio-attachment',
withContentBelow
@ -686,83 +693,82 @@ export class Message extends React.PureComponent<Props, State> {
<source src={firstAttachment.url} />
</audio>
);
} else {
const { pending, fileName, fileSize, contentType } = firstAttachment;
const extension = getExtensionForDisplay({ contentType, fileName });
const isDangerous = isFileDangerous(fileName || '');
}
const { pending, fileName, fileSize, contentType } = firstAttachment;
const extension = getExtensionForDisplay({ contentType, fileName });
const isDangerous = isFileDangerous(fileName || '');
return (
<button
className={classNames(
'module-message__generic-attachment',
withContentBelow
? 'module-message__generic-attachment--with-content-below'
: null,
withContentAbove
? 'module-message__generic-attachment--with-content-above'
: null,
!firstAttachment.url
? 'module-message__generic-attachment--not-active'
: null
)}
// There's only ever one of these, so we don't want users to tab into it
tabIndex={-1}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
return (
<button
type="button"
className={classNames(
'module-message__generic-attachment',
withContentBelow
? 'module-message__generic-attachment--with-content-below'
: null,
withContentAbove
? 'module-message__generic-attachment--with-content-above'
: null,
!firstAttachment.url
? 'module-message__generic-attachment--not-active'
: null
)}
// There's only ever one of these, so we don't want users to tab into it
tabIndex={-1}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
if (!firstAttachment.url) {
return;
}
if (!firstAttachment.url) {
return;
}
this.openGenericAttachment();
}}
>
{pending ? (
<div className="module-message__generic-attachment__spinner-container">
<Spinner svgSize="small" size="24px" direction={direction} />
</div>
) : (
<div className="module-message__generic-attachment__icon-container">
<div className="module-message__generic-attachment__icon">
{extension ? (
<div className="module-message__generic-attachment__icon__extension">
{extension}
</div>
) : null}
</div>
{isDangerous ? (
<div className="module-message__generic-attachment__icon-dangerous-container">
<div className="module-message__generic-attachment__icon-dangerous" />
this.openGenericAttachment();
}}
>
{pending ? (
<div className="module-message__generic-attachment__spinner-container">
<Spinner svgSize="small" size="24px" direction={direction} />
</div>
) : (
<div className="module-message__generic-attachment__icon-container">
<div className="module-message__generic-attachment__icon">
{extension ? (
<div className="module-message__generic-attachment__icon__extension">
{extension}
</div>
) : null}
</div>
)}
<div className="module-message__generic-attachment__text">
<div
className={classNames(
'module-message__generic-attachment__file-name',
`module-message__generic-attachment__file-name--${direction}`
)}
>
{fileName}
</div>
<div
className={classNames(
'module-message__generic-attachment__file-size',
`module-message__generic-attachment__file-size--${direction}`
)}
>
{fileSize}
</div>
{isDangerous ? (
<div className="module-message__generic-attachment__icon-dangerous-container">
<div className="module-message__generic-attachment__icon-dangerous" />
</div>
) : null}
</div>
</button>
);
}
)}
<div className="module-message__generic-attachment__text">
<div
className={classNames(
'module-message__generic-attachment__file-name',
`module-message__generic-attachment__file-name--${direction}`
)}
>
{fileName}
</div>
<div
className={classNames(
'module-message__generic-attachment__file-size',
`module-message__generic-attachment__file-size--${direction}`
)}
>
{fileSize}
</div>
</div>
</button>
);
}
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
public renderPreview() {
public renderPreview(): JSX.Element | null {
const {
attachments,
conversationType,
@ -809,6 +815,7 @@ export class Message extends React.PureComponent<Props, State> {
return (
<button
type="button"
className={classNames(
'module-message__link-preview',
`module-message__link-preview--${direction}`,
@ -835,7 +842,7 @@ export class Message extends React.PureComponent<Props, State> {
<ImageGrid
attachments={[first.image]}
withContentAbove={withContentAbove}
withContentBelow={true}
withContentBelow
onError={this.handleImageError}
i18n={i18n}
/>
@ -852,9 +859,9 @@ export class Message extends React.PureComponent<Props, State> {
<div className="module-message__link-preview__icon_container">
<Image
smallCurveTopLeft={!withContentAbove}
noBorder={true}
noBackground={true}
softCorners={true}
noBorder
noBackground
softCorners
alt={i18n('previewThumbnail', [first.domain])}
height={72}
width={72}
@ -900,7 +907,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
public renderQuote() {
public renderQuote(): JSX.Element | null {
const {
conversationType,
authorColor,
@ -952,7 +959,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
public renderEmbeddedContact() {
public renderEmbeddedContact(): JSX.Element | null {
const {
collapseMetadata,
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;
if (!contact || !contact.signalAccount) {
return null;
@ -997,6 +1004,7 @@ export class Message extends React.PureComponent<Props, State> {
return (
<button
type="button"
onClick={() => {
if (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 {
authorAvatarPath,
authorName,
@ -1031,6 +1039,7 @@ export class Message extends React.PureComponent<Props, State> {
return;
}
// eslint-disable-next-line consistent-return
return (
<div className="module-message__author-avatar">
<Avatar
@ -1048,7 +1057,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
public renderText() {
public renderText(): JSX.Element | null {
const {
bodyRanges,
deletedForEveryone,
@ -1060,6 +1069,7 @@ export class Message extends React.PureComponent<Props, State> {
textPending,
} = this.props;
// eslint-disable-next-line no-nested-ternary
const contents = deletedForEveryone
? i18n('message--deletedForEveryone')
: 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;
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 {
attachments,
// tslint:disable-next-line max-func-body-length
canReply,
direction,
disableMenu,
@ -1123,8 +1135,10 @@ export class Message extends React.PureComponent<Props, State> {
id,
isSticker,
isTapToView,
reactToMessage,
renderEmojiPicker,
replyToMessage,
selectedReaction,
} = this.props;
if (!isCorrectSide || disableMenu) {
@ -1142,10 +1156,13 @@ export class Message extends React.PureComponent<Props, State> {
!isTapToView &&
firstAttachment &&
!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
onClick={this.openGenericAttachment}
// This a menu meant for mouse use only
role="button"
aria-label={i18n('downloadAttachment')}
className={classNames(
'module-message__buttons__download',
`module-message__buttons__download--${direction}`
@ -1161,6 +1178,9 @@ export class Message extends React.PureComponent<Props, State> {
const maybePopperRef = isWide ? popperRef : undefined;
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
ref={maybePopperRef}
onClick={(event: React.MouseEvent) => {
@ -1171,6 +1191,7 @@ export class Message extends React.PureComponent<Props, State> {
}}
role="button"
className="module-message__buttons__react"
aria-label={i18n('reactToMessage')}
/>
);
}}
@ -1178,6 +1199,9 @@ export class Message extends React.PureComponent<Props, State> {
);
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
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
@ -1187,6 +1211,7 @@ export class Message extends React.PureComponent<Props, State> {
}}
// This a menu meant for mouse use only
role="button"
aria-label={i18n('replyToMessage')}
className={classNames(
'module-message__buttons__reply',
`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 = (
<Reference>
{({ ref: popperRef }) => {
@ -1205,13 +1233,14 @@ export class Message extends React.PureComponent<Props, State> {
return (
<ContextMenuTrigger
id={triggerId}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref={this.captureMenuTrigger as any}
>
<div
// This a menu meant for mouse use only
ref={maybePopperRef}
role="button"
onClick={this.showMenu}
aria-label={i18n('messageContextMenuButton')}
className={classNames(
'module-message__buttons__menu',
`module-message__buttons__download--${direction}`
@ -1222,6 +1251,8 @@ export class Message extends React.PureComponent<Props, State> {
}}
</Reference>
);
/* eslint-enable jsx-a11y/interactive-supports-focus */
/* eslint-enable jsx-a11y/click-events-have-key-events */
return (
<Manager>
@ -1238,19 +1269,20 @@ export class Message extends React.PureComponent<Props, State> {
</div>
{reactionPickerRoot &&
createPortal(
// eslint-disable-next-line consistent-return
<Popper placement="top">
{({ ref, style }) => (
<ReactionPicker
i18n={i18n}
ref={ref}
style={style}
selected={this.props.selectedReaction}
selected={selectedReaction}
onClose={this.toggleReactionPicker}
onPick={emoji => {
this.toggleReactionPicker(true);
this.props.reactToMessage(id, {
reactToMessage(id, {
emoji,
remove: emoji === this.props.selectedReaction,
remove: emoji === selectedReaction,
});
}}
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) {
public renderContextMenu(triggerId: string): JSX.Element {
const {
attachments,
canReply,
@ -1396,7 +1427,7 @@ export class Message extends React.PureComponent<Props, State> {
const first = previews[0];
if (!first || !first.image) {
return;
return undefined;
}
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() {
const { isTapToView, attachments, previews } = this.props;
const { imageBroken } = this.state;
@ -1449,7 +1482,7 @@ export class Message extends React.PureComponent<Props, State> {
return false;
}
public isAttachmentPending() {
public isAttachmentPending(): boolean {
const { attachments } = this.props;
if (!attachments || attachments.length < 1) {
@ -1461,7 +1494,7 @@ export class Message extends React.PureComponent<Props, State> {
return Boolean(first.pending);
}
public renderTapToViewIcon() {
public renderTapToViewIcon(): JSX.Element {
const { direction, isTapToViewExpired } = this.props;
const isDownloadPending = this.isAttachmentPending();
@ -1482,7 +1515,7 @@ export class Message extends React.PureComponent<Props, State> {
);
}
public renderTapToViewText() {
public renderTapToViewText(): string | undefined {
const {
attachments,
direction,
@ -1505,6 +1538,7 @@ export class Message extends React.PureComponent<Props, State> {
return;
}
// eslint-disable-next-line consistent-return, no-nested-ternary
return isTapToViewError
? i18n('incomingError')
: direction === 'outgoing'
@ -1512,7 +1546,7 @@ export class Message extends React.PureComponent<Props, State> {
: incomingString;
}
public renderTapToView() {
public renderTapToView(): JSX.Element {
const {
collapseMetadata,
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 }) => {
if (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 }) => {
if (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 { current: reactionsContainer } = this.reactionsContainerRef;
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;
if (reactionPickerRoot) {
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) {
public renderReactions(outgoing: boolean): JSX.Element | null {
const { reactions, i18n } = this.props;
if (!reactions || (reactions && reactions.length === 0)) {
@ -1726,6 +1759,8 @@ export class Message extends React.PureComponent<Props, State> {
return (
<button
type="button"
// eslint-disable-next-line react/no-array-index-key
key={`${re.emoji}-${i}`}
className={classNames(
'module-message__reactions__reaction',
@ -1764,7 +1799,7 @@ export class Message extends React.PureComponent<Props, State> {
+{maybeNotRenderedTotal}
</span>
) : (
<React.Fragment>
<>
<Emoji size={16} emoji={re.emoji} />
{re.count > 1 ? (
<span
@ -1778,7 +1813,7 @@ export class Message extends React.PureComponent<Props, State> {
{re.count}
</span>
) : null}
</React.Fragment>
</>
)}
</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;
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 = (
event: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent
) => {
): void => {
const {
attachments,
contact,
@ -1923,10 +1957,8 @@ export class Message extends React.PureComponent<Props, State> {
event.stopPropagation();
if (this.audioRef.current.paused) {
// tslint:disable-next-line no-floating-promises
this.audioRef.current.play();
} else {
// tslint:disable-next-line no-floating-promises
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;
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
const { canReply } = this.props;
@ -1989,7 +2021,7 @@ export class Message extends React.PureComponent<Props, State> {
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
const { text } = this.props;
if (text && text.length > 0) {
@ -2008,8 +2040,7 @@ export class Message extends React.PureComponent<Props, State> {
this.handleOpen(event);
};
// tslint:disable-next-line: cyclomatic-complexity
public renderContainer() {
public renderContainer(): JSX.Element {
const {
authorColor,
deletedForEveryone,
@ -2061,7 +2092,7 @@ export class Message extends React.PureComponent<Props, State> {
return (
<Measure
bounds={true}
bounds
onResize={({ bounds = { width: 0 } }) => {
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() {
public render(): JSX.Element | null {
const {
authorPhoneNumber,
attachments,

View File

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

View File

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

View File

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

View File

@ -37,10 +37,14 @@ export interface Props {
i18n: LocalizerType;
}
const _keyForError = (error: Error): string => {
return `${error.name}-${error.message}`;
};
export class MessageDetail extends React.Component<Props> {
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
// added off-screen and animated in. This ensures that the focus takes.
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 {
avatarPath,
@ -76,12 +80,13 @@ export class MessageDetail extends React.Component<Props> {
);
}
public renderDeleteButton() {
public renderDeleteButton(): JSX.Element {
const { i18n, message } = this.props;
return (
<div className="module-message-detail__delete-button-container">
<button
type="button"
onClick={() => {
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 errors = contact.errors || [];
const errorComponent = contact.isOutgoingKeyError ? (
<div className="module-message-detail__contact__error-buttons">
<button
type="button"
className="module-message-detail__contact__show-safety-number"
onClick={contact.onShowSafetyNumber}
>
{i18n('showSafetyNumber')}
</button>
<button
type="button"
className="module-message-detail__contact__send-anyway"
onClick={contact.onSendAnyway}
>
@ -138,8 +145,11 @@ export class MessageDetail extends React.Component<Props> {
i18n={i18n}
/>
</div>
{errors.map((error, index) => (
<div key={index} className="module-message-detail__contact__error">
{errors.map(error => (
<div
key={_keyForError(error)}
className="module-message-detail__contact__error"
>
{error.message}
</div>
))}
@ -151,7 +161,7 @@ export class MessageDetail extends React.Component<Props> {
);
}
public renderContacts() {
public renderContacts(): JSX.Element | null {
const { contacts } = this.props;
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;
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__message-container">
<Message i18n={i18n} {...message} />
</div>
<table className="module-message-detail__info">
<tbody>
{(errors || []).map((error, index) => (
<tr key={index}>
{(errors || []).map(error => (
<tr key={_keyForError(error)}>
<td className="module-message-detail__label">
{i18n('error')}
</td>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,8 @@
// tslint:disable:react-this-binding-issue
import React from 'react';
import classNames from 'classnames';
import * as MIME from '../../../ts/types/MIME';
import * as GoogleChrome from '../../../ts/util/GoogleChrome';
import * as MIME from '../../types/MIME';
import * as GoogleChrome from '../../util/GoogleChrome';
import { MessageBody } from './MessageBody';
import { BodyRangesType, LocalizerType } from '../../types/Util';
@ -65,7 +63,7 @@ function getObjectUrl(thumbnail: Attachment | undefined): string | undefined {
return thumbnail.objectUrl;
}
return;
return undefined;
}
function getTypeLabel({
@ -86,19 +84,21 @@ function getTypeLabel({
if (MIME.isAudio(contentType) && isVoiceMessage) {
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> {
public state = {
imageBroken: false,
};
constructor(props: Props) {
super(props);
this.state = {
imageBroken: false,
};
}
public handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
public handleKeyDown = (
event: React.KeyboardEvent<HTMLButtonElement>
): void => {
const { onClick } = this.props;
// 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();
}
};
public handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
public handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
const { onClick } = this.props;
if (onClick) {
@ -119,15 +120,20 @@ export class Quote extends React.Component<Props, State> {
}
};
public handleImageError = () => {
// tslint:disable-next-line no-console
console.log('Message: Image failed to load; failing over to placeholder');
public handleImageError = (): void => {
window.console.info(
'Message: Image failed to load; failing over to placeholder'
);
this.setState({
imageBroken: true,
});
};
public renderImage(url: string, i18n: LocalizerType, icon?: string) {
public renderImage(
url: string,
i18n: LocalizerType,
icon?: string
): JSX.Element {
const iconElement = icon ? (
<div className="module-quote__icon-container__inner">
<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 (
<div className="module-quote__icon-container">
<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;
if (!attachment) {
return;
return null;
}
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 { imageBroken } = this.state;
@ -283,8 +290,8 @@ export class Quote extends React.Component<Props, State> {
return null;
}
public renderClose() {
const { onClose } = this.props;
public renderClose(): JSX.Element | null {
const { i18n, onClose } = this.props;
if (!onClose) {
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
role="button"
className="module-quote__close-button"
aria-label={i18n('close')}
onKeyDown={keyDownHandler}
onClick={clickHandler}
/>
@ -320,7 +328,7 @@ export class Quote extends React.Component<Props, State> {
);
}
public renderAuthor() {
public renderAuthor(): JSX.Element {
const {
authorProfileName,
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;
if (!referencedMessageNotFound) {
@ -389,7 +397,7 @@ export class Quote extends React.Component<Props, State> {
);
}
public render() {
public render(): JSX.Element | null {
const {
authorColor,
isIncoming,
@ -410,6 +418,7 @@ export class Quote extends React.Component<Props, State> {
)}
>
<button
type="button"
onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
className={classNames(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,46 +27,49 @@ export type PropsActions = {
export type Props = PropsData & PropsHousekeeping & PropsActions;
export class SafetyNumberNotification extends React.Component<Props> {
public render() {
const { contact, isGroup, i18n, showIdentity } = this.props;
const changeKey = isGroup
? 'safetyNumberChangedGroup'
: 'safetyNumberChanged';
export const SafetyNumberNotification = ({
contact,
isGroup,
i18n,
showIdentity,
}: Props): JSX.Element => {
const changeKey = isGroup
? 'safetyNumberChangedGroup'
: 'safetyNumberChanged';
return (
<div className="module-safety-number-notification">
<div className="module-safety-number-notification__icon" />
<div className="module-safety-number-notification__text">
<Intl
id={changeKey}
components={[
<span
key="external-1"
className="module-safety-number-notification__contact"
>
<ContactName
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-safety-number-notification__contact"
i18n={i18n}
/>
</span>,
]}
i18n={i18n}
/>
</div>
<button
onClick={() => {
showIdentity(contact.id);
}}
className="module-safety-number-notification__button"
>
{i18n('verifyNewNumber')}
</button>
return (
<div className="module-safety-number-notification">
<div className="module-safety-number-notification__icon" />
<div className="module-safety-number-notification__text">
<Intl
id={changeKey}
components={[
<span
key="external-1"
className="module-safety-number-notification__contact"
>
<ContactName
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-safety-number-notification__contact"
i18n={i18n}
/>
</span>,
]}
i18n={i18n}
/>
</div>
);
}
}
<button
type="button"
onClick={() => {
showIdentity(contact.id);
}}
className="module-safety-number-notification__button"
>
{i18n('verifyNewNumber')}
</button>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,48 +16,53 @@ export interface Props {
onClose?: () => void;
}
export class StagedLinkPreview extends React.Component<Props> {
public render() {
const { isLoaded, onClose, i18n, title, image, domain } = this.props;
export const StagedLinkPreview = ({
isLoaded,
onClose,
i18n,
title,
image,
domain,
}: Props): JSX.Element => {
const isImage = image && isImageAttachment(image);
const isImage = image && isImageAttachment(image);
return (
<div
className={classNames(
'module-staged-link-preview',
!isLoaded ? 'module-staged-link-preview--is-loading' : null
)}
>
{!isLoaded ? (
<div className="module-staged-link-preview__loading">
{i18n('loadingPreview')}
</div>
) : null}
{isLoaded && image && isImage ? (
<div className="module-staged-link-preview__icon-container">
<Image
alt={i18n('stagedPreviewThumbnail', [domain])}
softCorners={true}
height={72}
width={72}
url={image.url}
attachment={image}
i18n={i18n}
/>
</div>
) : null}
{isLoaded ? (
<div className="module-staged-link-preview__content">
<div className="module-staged-link-preview__title">{title}</div>
<div className="module-staged-link-preview__location">{domain}</div>
</div>
) : null}
<button
className="module-staged-link-preview__close-button"
onClick={onClose}
/>
</div>
);
}
}
return (
<div
className={classNames(
'module-staged-link-preview',
!isLoaded ? 'module-staged-link-preview--is-loading' : null
)}
>
{!isLoaded ? (
<div className="module-staged-link-preview__loading">
{i18n('loadingPreview')}
</div>
) : null}
{isLoaded && image && isImage ? (
<div className="module-staged-link-preview__icon-container">
<Image
alt={i18n('stagedPreviewThumbnail', [domain])}
softCorners
height={72}
width={72}
url={image.url}
attachment={image}
i18n={i18n}
/>
</div>
) : null}
{isLoaded ? (
<div className="module-staged-link-preview__content">
<div className="module-staged-link-preview__title">{title}</div>
<div className="module-staged-link-preview__location">{domain}</div>
</div>
) : null}
<button
type="button"
className="module-staged-link-preview__close-button"
onClick={onClose}
aria-label={i18n('close')}
/>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,18 +16,15 @@ export type Props = {
const FAKE_DURATION = 1000;
export class TimelineLoadingRow extends React.PureComponent<Props> {
public renderContents() {
public renderContents(): JSX.Element {
const { state, duration, expiresAt, onComplete } = this.props;
if (state === 'idle') {
const fakeExpiresAt = Date.now() - FAKE_DURATION;
return <Countdown duration={FAKE_DURATION} expiresAt={fakeExpiresAt} />;
} else if (
state === 'countdown' &&
isNumber(duration) &&
isNumber(expiresAt)
) {
}
if (state === 'countdown' && isNumber(duration) && isNumber(expiresAt)) {
return (
<Countdown
duration={duration}
@ -40,7 +37,7 @@ export class TimelineLoadingRow extends React.PureComponent<Props> {
return <Spinner size="24" svgSize="small" direction="on-background" />;
}
public render() {
public render(): JSX.Element {
return (
<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 { boolean, select, text } from '@storybook/addon-knobs';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json';
import { Props, TimerNotification } from './TimerNotification';
const i18n = setupI18n('en', enMessages);

View File

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

View File

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

View File

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

View File

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

View File

@ -8,36 +8,30 @@ export interface Props {
color?: string;
}
export class TypingAnimation extends React.Component<Props> {
public render() {
const { i18n, color } = this.props;
return (
<div className="module-typing-animation" title={i18n('typingAlt')}>
<div
className={classNames(
'module-typing-animation__dot',
'module-typing-animation__dot--first',
color ? `module-typing-animation__dot--${color}` : null
)}
/>
<div className="module-typing-animation__spacer" />
<div
className={classNames(
'module-typing-animation__dot',
'module-typing-animation__dot--second',
color ? `module-typing-animation__dot--${color}` : null
)}
/>
<div className="module-typing-animation__spacer" />
<div
className={classNames(
'module-typing-animation__dot',
'module-typing-animation__dot--third',
color ? `module-typing-animation__dot--${color}` : null
)}
/>
</div>
);
}
}
export const TypingAnimation = ({ i18n, color }: Props): JSX.Element => (
<div className="module-typing-animation" title={i18n('typingAlt')}>
<div
className={classNames(
'module-typing-animation__dot',
'module-typing-animation__dot--first',
color ? `module-typing-animation__dot--${color}` : null
)}
/>
<div className="module-typing-animation__spacer" />
<div
className={classNames(
'module-typing-animation__dot',
'module-typing-animation__dot--second',
color ? `module-typing-animation__dot--${color}` : null
)}
/>
<div className="module-typing-animation__spacer" />
<div
className={classNames(
'module-typing-animation__dot',
'module-typing-animation__dot--third',
color ? `module-typing-animation__dot--${color}` : null
)}
/>
</div>
);

View File

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

View File

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

View File

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

View File

@ -29,61 +29,62 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping & PropsActions;
export class UnsupportedMessage extends React.Component<Props> {
public render() {
const { canProcessNow, contact, i18n, downloadNewVersion } = this.props;
const { isMe } = contact;
export const UnsupportedMessage = ({
canProcessNow,
contact,
i18n,
downloadNewVersion,
}: Props): JSX.Element => {
const { isMe } = contact;
const otherStringId = canProcessNow
? 'Message--unsupported-message-ask-to-resend'
: 'Message--unsupported-message';
const meStringId = canProcessNow
? 'Message--from-me-unsupported-message-ask-to-resend'
: 'Message--from-me-unsupported-message';
const stringId = isMe ? meStringId : otherStringId;
const otherStringId = canProcessNow
? 'Message--unsupported-message-ask-to-resend'
: 'Message--unsupported-message';
const meStringId = canProcessNow
? 'Message--from-me-unsupported-message-ask-to-resend'
: 'Message--from-me-unsupported-message';
const stringId = isMe ? meStringId : otherStringId;
return (
<div className="module-unsupported-message">
<div
className={classNames(
'module-unsupported-message__icon',
canProcessNow
? 'module-unsupported-message__icon--can-process'
: null
)}
/>
<div className="module-unsupported-message__text">
<Intl
id={stringId}
components={[
<span
key="external-1"
className="module-unsupported-message__contact"
>
<ContactName
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-unsupported-message__contact"
i18n={i18n}
/>
</span>,
]}
i18n={i18n}
/>
</div>
{canProcessNow ? null : (
<button
onClick={() => {
downloadNewVersion();
}}
className="module-unsupported-message__button"
>
{i18n('Message--update-signal')}
</button>
return (
<div className="module-unsupported-message">
<div
className={classNames(
'module-unsupported-message__icon',
canProcessNow ? 'module-unsupported-message__icon--can-process' : null
)}
/>
<div className="module-unsupported-message__text">
<Intl
id={stringId}
components={[
<span
key="external-1"
className="module-unsupported-message__contact"
>
<ContactName
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}
title={contact.title}
module="module-unsupported-message__contact"
i18n={i18n}
/>
</span>,
]}
i18n={i18n}
/>
</div>
);
}
}
{canProcessNow ? null : (
<button
type="button"
onClick={() => {
downloadNewVersion();
}}
className="module-unsupported-message__button"
>
{i18n('Message--update-signal')}
</button>
)}
</div>
);
};

View File

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

View File

@ -27,7 +27,7 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping;
export class VerificationNotification extends React.Component<Props> {
public getStringId() {
public getStringId(): string {
const { isLocal, type } = this.props;
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 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 suffix =
type === 'markVerified' ? 'mark-verified' : 'mark-not-verified';

View File

@ -19,7 +19,7 @@ export function renderAvatar({
i18n: LocalizerType;
size: 28 | 52 | 80;
direction?: 'outgoing' | 'incoming';
}) {
}): JSX.Element {
const { avatar } = contact;
const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
@ -60,7 +60,7 @@ export function renderName({
contact: ContactType;
isIncoming: boolean;
module: string;
}) {
}): JSX.Element {
return (
<div
className={classNames(
@ -81,7 +81,7 @@ export function renderContactShorthand({
contact: ContactType;
isIncoming: boolean;
module: string;
}) {
}): JSX.Element {
const { number: phoneNumber, email } = contact;
const firstNumber = phoneNumber && phoneNumber[0] && phoneNumber[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 { storiesOf } from '@storybook/react';
import { select, text, withKnobs } from '@storybook/addon-knobs';
import { random, range, sample, sortBy } from 'lodash';
// @ts-ignore
import { setup as setupI18n } from '../../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../../_locales/en/messages.json';
import { MIMEType } from '../../../types/MIME';
import { MediaItemType } from '../../LightboxGallery';
@ -20,6 +19,7 @@ const story = storiesOf(
module
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
export const now = Date.now();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,7 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../../_locales/en/messages.json';
import {

View File

@ -48,6 +48,8 @@ const Tab = ({
: undefined;
return (
// Has key events handled elsewhere
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div
className={classNames(
'module-media-gallery__tab',
@ -64,11 +66,15 @@ const Tab = ({
export class MediaGallery extends React.Component<Props, State> {
public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();
public state: State = {
selectedTab: 'media',
};
public componentDidMount() {
constructor(props: Props) {
super(props);
this.state = {
selectedTab: 'media',
};
}
public componentDidMount(): void {
// 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.
setTimeout(() => {
@ -78,7 +84,7 @@ export class MediaGallery extends React.Component<Props, State> {
});
}
public render() {
public render(): JSX.Element {
const { selectedTab } = this.state;
return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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