Migrate conversations to ESLint
This commit is contained in:
parent
b4f0f3c685
commit
372aa44e49
|
@ -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/**
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -27,18 +27,14 @@ 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 {
|
||||
export const AttachmentList = ({
|
||||
attachments,
|
||||
i18n,
|
||||
onAddAttachment,
|
||||
onClickAttachment,
|
||||
onCloseAttachment,
|
||||
onClose,
|
||||
} = this.props;
|
||||
|
||||
}: Props): JSX.Element | null => {
|
||||
if (!attachments.length) {
|
||||
return null;
|
||||
}
|
||||
|
@ -50,8 +46,10 @@ export class AttachmentList extends React.Component<Props> {
|
|||
{attachments.length > 1 ? (
|
||||
<div className="module-attachments__header">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="module-attachments__close-button"
|
||||
aria-label={i18n('close')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -62,8 +60,7 @@ export class AttachmentList extends React.Component<Props> {
|
|||
isImageTypeSupported(contentType) ||
|
||||
isVideoTypeSupported(contentType)
|
||||
) {
|
||||
const imageKey =
|
||||
getUrl(attachment) || attachment.fileName || index;
|
||||
const imageKey = getUrl(attachment) || attachment.fileName || index;
|
||||
const clickCallback =
|
||||
attachments.length > 1 ? onClickAttachment : undefined;
|
||||
|
||||
|
@ -75,12 +72,12 @@ export class AttachmentList extends React.Component<Props> {
|
|||
])}
|
||||
i18n={i18n}
|
||||
attachment={attachment}
|
||||
softCorners={true}
|
||||
softCorners
|
||||
playIconOverlay={isVideoAttachment(attachment)}
|
||||
height={IMAGE_HEIGHT}
|
||||
width={IMAGE_WIDTH}
|
||||
url={getUrl(attachment)}
|
||||
closeButton={true}
|
||||
closeButton
|
||||
onClick={clickCallback}
|
||||
onClickClose={onCloseAttachment}
|
||||
onError={() => {
|
||||
|
@ -90,8 +87,7 @@ export class AttachmentList extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
const genericKey =
|
||||
getUrl(attachment) || attachment.fileName || index;
|
||||
const genericKey = getUrl(attachment) || attachment.fileName || index;
|
||||
|
||||
return (
|
||||
<StagedGenericAttachment
|
||||
|
@ -103,13 +99,9 @@ export class AttachmentList extends React.Component<Props> {
|
|||
);
|
||||
})}
|
||||
{allVisualAttachments ? (
|
||||
<StagedPlaceholderAttachment
|
||||
onClick={onAddAttachment}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<StagedPlaceholderAttachment onClick={onAddAttachment} i18n={i18n} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -31,38 +31,31 @@ export function getCallingNotificationText(
|
|||
if (wasDeclined) {
|
||||
if (wasVideoCall) {
|
||||
return i18n('declinedIncomingVideoCall');
|
||||
} else {
|
||||
}
|
||||
return i18n('declinedIncomingAudioCall');
|
||||
}
|
||||
} else if (wasAccepted) {
|
||||
if (wasAccepted) {
|
||||
if (wasVideoCall) {
|
||||
return i18n('acceptedIncomingVideoCall');
|
||||
} else {
|
||||
}
|
||||
return i18n('acceptedIncomingAudioCall');
|
||||
}
|
||||
} else {
|
||||
if (wasVideoCall) {
|
||||
return i18n('missedIncomingVideoCall');
|
||||
} else {
|
||||
}
|
||||
return i18n('missedIncomingAudioCall');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (wasAccepted) {
|
||||
if (wasVideoCall) {
|
||||
return i18n('acceptedOutgoingVideoCall');
|
||||
} else {
|
||||
}
|
||||
return i18n('acceptedOutgoingAudioCall');
|
||||
}
|
||||
} else {
|
||||
if (wasVideoCall) {
|
||||
return i18n('missedOrDeclinedOutgoingVideoCall');
|
||||
} else {
|
||||
}
|
||||
return i18n('missedOrDeclinedOutgoingAudioCall');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CallingNotification = (props: Props): JSX.Element | null => {
|
||||
const { callHistoryDetails, i18n } = props;
|
||||
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`;
|
||||
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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 */
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -30,10 +30,7 @@ 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 {
|
||||
export const ImageGrid = ({
|
||||
attachments,
|
||||
bottomOverlay,
|
||||
i18n,
|
||||
|
@ -44,12 +41,11 @@ export class ImageGrid extends React.Component<Props> {
|
|||
tabIndex,
|
||||
withContentAbove,
|
||||
withContentBelow,
|
||||
} = this.props;
|
||||
|
||||
const curveTopLeft = !Boolean(withContentAbove);
|
||||
}: Props): JSX.Element | null => {
|
||||
const curveTopLeft = !withContentAbove;
|
||||
const curveTopRight = curveTopLeft;
|
||||
|
||||
const curveBottom = !Boolean(withContentBelow);
|
||||
const curveBottom = !withContentBelow;
|
||||
const curveBottomLeft = curveBottom;
|
||||
const curveBottomRight = curveBottom;
|
||||
|
||||
|
@ -347,5 +343,4 @@ export class ImageGrid extends React.Component<Props> {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -7,10 +7,7 @@ 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')
|
||||
|
@ -22,5 +19,4 @@ export class LastSeenIndicator extends React.Component<Props> {
|
|||
<div className="module-last-seen-indicator__text">{message}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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!',
|
||||
|
|
|
@ -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,13 +693,14 @@ 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 || '');
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-message__generic-attachment',
|
||||
withContentBelow
|
||||
|
@ -759,10 +767,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
</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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -94,7 +94,7 @@ export class MessageBody extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
const {
|
||||
bodyRanges,
|
||||
text,
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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 = {
|
||||
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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -6,14 +6,8 @@ export interface Props {
|
|||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class ResetSessionNotification extends React.Component<Props> {
|
||||
public render() {
|
||||
const { i18n } = this.props;
|
||||
|
||||
return (
|
||||
export const ResetSessionNotification = ({ i18n }: Props): JSX.Element => (
|
||||
<div className="module-reset-session-notification">
|
||||
{i18n('sessionEnded')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -27,9 +27,12 @@ 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;
|
||||
export const SafetyNumberNotification = ({
|
||||
contact,
|
||||
isGroup,
|
||||
i18n,
|
||||
showIdentity,
|
||||
}: Props): JSX.Element => {
|
||||
const changeKey = isGroup
|
||||
? 'safetyNumberChangedGroup'
|
||||
: 'safetyNumberChanged';
|
||||
|
@ -59,6 +62,7 @@ export class SafetyNumberNotification extends React.Component<Props> {
|
|||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
showIdentity(contact.id);
|
||||
}}
|
||||
|
@ -68,5 +72,4 @@ export class SafetyNumberNotification extends React.Component<Props> {
|
|||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -12,16 +12,18 @@ 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
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-scroll-down__button',
|
||||
withNewMessages ? 'module-scroll-down__button--new-messages' : null
|
||||
|
@ -35,5 +37,4 @@ export class ScrollDownButton extends React.Component<Props> {
|
|||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -9,16 +9,20 @@ export interface Props {
|
|||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class StagedGenericAttachment extends React.Component<Props> {
|
||||
public render() {
|
||||
const { attachment, onClose } = this.props;
|
||||
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
|
||||
type="button"
|
||||
className="module-staged-generic-attachment__close-button"
|
||||
aria-label={i18n('close')}
|
||||
onClick={() => {
|
||||
if (onClose) {
|
||||
onClose(attachment);
|
||||
|
@ -37,5 +41,4 @@ export class StagedGenericAttachment extends React.Component<Props> {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -16,10 +16,14 @@ 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);
|
||||
|
||||
return (
|
||||
|
@ -38,7 +42,7 @@ export class StagedLinkPreview extends React.Component<Props> {
|
|||
<div className="module-staged-link-preview__icon-container">
|
||||
<Image
|
||||
alt={i18n('stagedPreviewThumbnail', [domain])}
|
||||
softCorners={true}
|
||||
softCorners
|
||||
height={72}
|
||||
width={72}
|
||||
url={image.url}
|
||||
|
@ -54,10 +58,11 @@ export class StagedLinkPreview extends React.Component<Props> {
|
|||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="module-staged-link-preview__close-button"
|
||||
onClick={onClose}
|
||||
aria-label={i18n('close')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -6,12 +6,12 @@ interface Props {
|
|||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class StagedPlaceholderAttachment extends React.Component<Props> {
|
||||
public render() {
|
||||
const { i18n, onClick } = this.props;
|
||||
|
||||
return (
|
||||
export const StagedPlaceholderAttachment = ({
|
||||
i18n,
|
||||
onClick,
|
||||
}: Props): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
className="module-staged-placeholder-attachment"
|
||||
onClick={onClick}
|
||||
title={i18n('add-image-attachment')}
|
||||
|
@ -19,5 +19,3 @@ export class StagedPlaceholderAttachment extends React.Component<Props> {
|
|||
<div className="module-staged-placeholder-attachment__plus-icon" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = () => ({
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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: {
|
||||
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'];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,6 +66,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
|
||||
const createTable = (overrideProps: Partial<Props> = {}) => (
|
||||
<table cellPadding={5}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Timestamp</th>
|
||||
|
@ -85,6 +82,7 @@ const createTable = (overrideProps: Partial<Props> = {}) => (
|
|||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -8,11 +8,7 @@ export interface Props {
|
|||
color?: string;
|
||||
}
|
||||
|
||||
export class TypingAnimation extends React.Component<Props> {
|
||||
public render() {
|
||||
const { i18n, color } = this.props;
|
||||
|
||||
return (
|
||||
export const TypingAnimation = ({ i18n, color }: Props): JSX.Element => (
|
||||
<div className="module-typing-animation" title={i18n('typingAlt')}>
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -39,5 +35,3 @@ export class TypingAnimation extends React.Component<Props> {
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -29,9 +29,12 @@ type PropsHousekeeping = {
|
|||
|
||||
export type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||
|
||||
export class UnsupportedMessage extends React.Component<Props> {
|
||||
public render() {
|
||||
const { canProcessNow, contact, i18n, downloadNewVersion } = this.props;
|
||||
export const UnsupportedMessage = ({
|
||||
canProcessNow,
|
||||
contact,
|
||||
i18n,
|
||||
downloadNewVersion,
|
||||
}: Props): JSX.Element => {
|
||||
const { isMe } = contact;
|
||||
|
||||
const otherStringId = canProcessNow
|
||||
|
@ -47,9 +50,7 @@ export class UnsupportedMessage extends React.Component<Props> {
|
|||
<div
|
||||
className={classNames(
|
||||
'module-unsupported-message__icon',
|
||||
canProcessNow
|
||||
? 'module-unsupported-message__icon--can-process'
|
||||
: null
|
||||
canProcessNow ? 'module-unsupported-message__icon--can-process' : null
|
||||
)}
|
||||
/>
|
||||
<div className="module-unsupported-message__text">
|
||||
|
@ -75,6 +76,7 @@ export class UnsupportedMessage extends React.Component<Props> {
|
|||
</div>
|
||||
{canProcessNow ? null : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
downloadNewVersion();
|
||||
}}
|
||||
|
@ -85,5 +87,4 @@ export class UnsupportedMessage extends React.Component<Props> {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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', () => (
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
export const LoadingIndicator = () => {
|
||||
export const LoadingIndicator = (): JSX.Element => {
|
||||
return (
|
||||
<div className="loading-widget">
|
||||
<div className="container">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedTab: 'media',
|
||||
};
|
||||
}
|
||||
|
||||
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(() => {
|
||||
|
@ -78,7 +84,7 @@ export class MediaGallery extends React.Component<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
const { selectedTab } = this.state;
|
||||
|
||||
return (
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -181,6 +181,7 @@
|
|||
"ts/backbone/**",
|
||||
"ts/build/**",
|
||||
"ts/components/*.ts[x]",
|
||||
"ts/components/conversation/**",
|
||||
"ts/components/emoji/**",
|
||||
"ts/notifications/**",
|
||||
"ts/protobuf/**",
|
||||
|
|
Loading…
Reference in New Issue