Migrate conversations to ESLint
This commit is contained in:
parent
b4f0f3c685
commit
372aa44e49
90 changed files with 1261 additions and 1165 deletions
|
@ -33,7 +33,8 @@ webpack.config.ts
|
||||||
sticker-creator/**/*.ts
|
sticker-creator/**/*.ts
|
||||||
sticker-creator/**/*.tsx
|
sticker-creator/**/*.tsx
|
||||||
ts/*.ts
|
ts/*.ts
|
||||||
ts/components/conversation/**
|
ts/components/*.ts
|
||||||
|
ts/components/*.tsx
|
||||||
ts/components/stickers/**
|
ts/components/stickers/**
|
||||||
ts/shims/**
|
ts/shims/**
|
||||||
ts/sql/**
|
ts/sql/**
|
||||||
|
|
|
@ -118,6 +118,8 @@ module.exports = {
|
||||||
rules: {
|
rules: {
|
||||||
...rules,
|
...rules,
|
||||||
'import/no-extraneous-dependencies': 'off',
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'react/jsx-props-no-spreading': 'off',
|
||||||
|
'react/no-array-index-key': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -151,6 +151,10 @@
|
||||||
"message": "Set Up as Standalone Device",
|
"message": "Set Up as Standalone Device",
|
||||||
"description": "Only available on development modes, menu option to open up the standalone device setup sequence"
|
"description": "Only available on development modes, menu option to open up the standalone device setup sequence"
|
||||||
},
|
},
|
||||||
|
"messageContextMenuButton": {
|
||||||
|
"message": "More actions",
|
||||||
|
"description": "Label for context button next to each message"
|
||||||
|
},
|
||||||
"contextMenuCopyLink": {
|
"contextMenuCopyLink": {
|
||||||
"message": "Copy Link",
|
"message": "Copy Link",
|
||||||
"description": "Shown in the context menu for a link to indicate that the user can copy the link"
|
"description": "Shown in the context menu for a link to indicate that the user can copy the link"
|
||||||
|
@ -985,6 +989,10 @@
|
||||||
"theirIdentityUnknown": {
|
"theirIdentityUnknown": {
|
||||||
"message": "You haven't exchanged any messages with this contact yet. Your safety number with them will be available after the first message."
|
"message": "You haven't exchanged any messages with this contact yet. Your safety number with them will be available after the first message."
|
||||||
},
|
},
|
||||||
|
"goBack": {
|
||||||
|
"message": "Go back",
|
||||||
|
"description": "Label for back button in a conversation"
|
||||||
|
},
|
||||||
"moreInfo": {
|
"moreInfo": {
|
||||||
"message": "More Info...",
|
"message": "More Info...",
|
||||||
"description": "Shown on the drop-down menu for an individual message, takes you to message detail screen"
|
"description": "Shown on the drop-down menu for an individual message, takes you to message detail screen"
|
||||||
|
@ -2772,6 +2780,14 @@
|
||||||
"message": "Ringing...",
|
"message": "Ringing...",
|
||||||
"description": "Shown in the call screen when placing an outgoing call that is now ringing"
|
"description": "Shown in the call screen when placing an outgoing call that is now ringing"
|
||||||
},
|
},
|
||||||
|
"makeOutgoingCall": {
|
||||||
|
"message": "Start a call",
|
||||||
|
"description": "Title for the call button in a conversation"
|
||||||
|
},
|
||||||
|
"makeOutgoingVideoCall": {
|
||||||
|
"message": "Start a video call",
|
||||||
|
"description": "Title for the video call button in a conversation"
|
||||||
|
},
|
||||||
"callReconnecting": {
|
"callReconnecting": {
|
||||||
"message": "Reconnecting...",
|
"message": "Reconnecting...",
|
||||||
"description": "Shown in the call screen when the call is reconnecting due to network issues"
|
"description": "Shown in the call screen when the call is reconnecting due to network issues"
|
||||||
|
@ -3574,7 +3590,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"close": {
|
"close": {
|
||||||
"message": "close",
|
"message": "Close",
|
||||||
"description": "Generic close label"
|
"description": "Generic close label"
|
||||||
},
|
},
|
||||||
"previous": {
|
"previous": {
|
||||||
|
|
|
@ -13,15 +13,20 @@ export class AddNewLines extends React.Component<Props> {
|
||||||
renderNonNewLine: ({ text }) => text,
|
renderNonNewLine: ({ text }) => text,
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render():
|
||||||
|
| JSX.Element
|
||||||
|
| string
|
||||||
|
| null
|
||||||
|
| Array<JSX.Element | string | null> {
|
||||||
const { text, renderNonNewLine } = this.props;
|
const { text, renderNonNewLine } = this.props;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const results: Array<any> = [];
|
const results: Array<any> = [];
|
||||||
const FIND_NEWLINES = /\n/g;
|
const FIND_NEWLINES = /\n/g;
|
||||||
|
|
||||||
// We have to do this, because renderNonNewLine is not required in our Props object,
|
// We have to do this, because renderNonNewLine is not required in our Props object,
|
||||||
// but it is always provided via defaultProps.
|
// but it is always provided via defaultProps.
|
||||||
if (!renderNonNewLine) {
|
if (!renderNonNewLine) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let match = FIND_NEWLINES.exec(text);
|
let match = FIND_NEWLINES.exec(text);
|
||||||
|
@ -35,20 +40,20 @@ export class AddNewLines extends React.Component<Props> {
|
||||||
while (match) {
|
while (match) {
|
||||||
if (last < match.index) {
|
if (last < match.index) {
|
||||||
const textWithNoNewline = text.slice(last, match.index);
|
const textWithNoNewline = text.slice(last, match.index);
|
||||||
results.push(
|
count += 1;
|
||||||
renderNonNewLine({ text: textWithNoNewline, key: count++ })
|
results.push(renderNonNewLine({ text: textWithNoNewline, key: count }));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push(<br key={count++} />);
|
count += 1;
|
||||||
|
results.push(<br key={count} />);
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
last = FIND_NEWLINES.lastIndex;
|
last = FIND_NEWLINES.lastIndex;
|
||||||
match = FIND_NEWLINES.exec(text);
|
match = FIND_NEWLINES.exec(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last < text.length) {
|
if (last < text.length) {
|
||||||
results.push(renderNonNewLine({ text: text.slice(last), key: count++ }));
|
count += 1;
|
||||||
|
results.push(renderNonNewLine({ text: text.slice(last), key: count }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
|
@ -11,11 +11,9 @@ import {
|
||||||
MIMEType,
|
MIMEType,
|
||||||
VIDEO_MP4,
|
VIDEO_MP4,
|
||||||
} from '../../types/MIME';
|
} from '../../types/MIME';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/AttachmentList', module);
|
const story = storiesOf('Components/Conversation/AttachmentList', module);
|
||||||
|
|
|
@ -27,18 +27,14 @@ export interface Props {
|
||||||
const IMAGE_WIDTH = 120;
|
const IMAGE_WIDTH = 120;
|
||||||
const IMAGE_HEIGHT = 120;
|
const IMAGE_HEIGHT = 120;
|
||||||
|
|
||||||
export class AttachmentList extends React.Component<Props> {
|
export const AttachmentList = ({
|
||||||
// tslint:disable-next-line max-func-body-length */
|
|
||||||
public render() {
|
|
||||||
const {
|
|
||||||
attachments,
|
attachments,
|
||||||
i18n,
|
i18n,
|
||||||
onAddAttachment,
|
onAddAttachment,
|
||||||
onClickAttachment,
|
onClickAttachment,
|
||||||
onCloseAttachment,
|
onCloseAttachment,
|
||||||
onClose,
|
onClose,
|
||||||
} = this.props;
|
}: Props): JSX.Element | null => {
|
||||||
|
|
||||||
if (!attachments.length) {
|
if (!attachments.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -50,8 +46,10 @@ export class AttachmentList extends React.Component<Props> {
|
||||||
{attachments.length > 1 ? (
|
{attachments.length > 1 ? (
|
||||||
<div className="module-attachments__header">
|
<div className="module-attachments__header">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="module-attachments__close-button"
|
className="module-attachments__close-button"
|
||||||
|
aria-label={i18n('close')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -62,8 +60,7 @@ export class AttachmentList extends React.Component<Props> {
|
||||||
isImageTypeSupported(contentType) ||
|
isImageTypeSupported(contentType) ||
|
||||||
isVideoTypeSupported(contentType)
|
isVideoTypeSupported(contentType)
|
||||||
) {
|
) {
|
||||||
const imageKey =
|
const imageKey = getUrl(attachment) || attachment.fileName || index;
|
||||||
getUrl(attachment) || attachment.fileName || index;
|
|
||||||
const clickCallback =
|
const clickCallback =
|
||||||
attachments.length > 1 ? onClickAttachment : undefined;
|
attachments.length > 1 ? onClickAttachment : undefined;
|
||||||
|
|
||||||
|
@ -75,12 +72,12 @@ export class AttachmentList extends React.Component<Props> {
|
||||||
])}
|
])}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
attachment={attachment}
|
attachment={attachment}
|
||||||
softCorners={true}
|
softCorners
|
||||||
playIconOverlay={isVideoAttachment(attachment)}
|
playIconOverlay={isVideoAttachment(attachment)}
|
||||||
height={IMAGE_HEIGHT}
|
height={IMAGE_HEIGHT}
|
||||||
width={IMAGE_WIDTH}
|
width={IMAGE_WIDTH}
|
||||||
url={getUrl(attachment)}
|
url={getUrl(attachment)}
|
||||||
closeButton={true}
|
closeButton
|
||||||
onClick={clickCallback}
|
onClick={clickCallback}
|
||||||
onClickClose={onCloseAttachment}
|
onClickClose={onCloseAttachment}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
|
@ -90,8 +87,7 @@ export class AttachmentList extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const genericKey =
|
const genericKey = getUrl(attachment) || attachment.fileName || index;
|
||||||
getUrl(attachment) || attachment.fileName || index;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StagedGenericAttachment
|
<StagedGenericAttachment
|
||||||
|
@ -103,13 +99,9 @@ export class AttachmentList extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{allVisualAttachments ? (
|
{allVisualAttachments ? (
|
||||||
<StagedPlaceholderAttachment
|
<StagedPlaceholderAttachment onClick={onAddAttachment} i18n={i18n} />
|
||||||
onClick={onAddAttachment}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -31,38 +31,31 @@ export function getCallingNotificationText(
|
||||||
if (wasDeclined) {
|
if (wasDeclined) {
|
||||||
if (wasVideoCall) {
|
if (wasVideoCall) {
|
||||||
return i18n('declinedIncomingVideoCall');
|
return i18n('declinedIncomingVideoCall');
|
||||||
} else {
|
}
|
||||||
return i18n('declinedIncomingAudioCall');
|
return i18n('declinedIncomingAudioCall');
|
||||||
}
|
}
|
||||||
} else if (wasAccepted) {
|
if (wasAccepted) {
|
||||||
if (wasVideoCall) {
|
if (wasVideoCall) {
|
||||||
return i18n('acceptedIncomingVideoCall');
|
return i18n('acceptedIncomingVideoCall');
|
||||||
} else {
|
}
|
||||||
return i18n('acceptedIncomingAudioCall');
|
return i18n('acceptedIncomingAudioCall');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (wasVideoCall) {
|
if (wasVideoCall) {
|
||||||
return i18n('missedIncomingVideoCall');
|
return i18n('missedIncomingVideoCall');
|
||||||
} else {
|
}
|
||||||
return i18n('missedIncomingAudioCall');
|
return i18n('missedIncomingAudioCall');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (wasAccepted) {
|
if (wasAccepted) {
|
||||||
if (wasVideoCall) {
|
if (wasVideoCall) {
|
||||||
return i18n('acceptedOutgoingVideoCall');
|
return i18n('acceptedOutgoingVideoCall');
|
||||||
} else {
|
}
|
||||||
return i18n('acceptedOutgoingAudioCall');
|
return i18n('acceptedOutgoingAudioCall');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (wasVideoCall) {
|
if (wasVideoCall) {
|
||||||
return i18n('missedOrDeclinedOutgoingVideoCall');
|
return i18n('missedOrDeclinedOutgoingVideoCall');
|
||||||
} else {
|
}
|
||||||
return i18n('missedOrDeclinedOutgoingAudioCall');
|
return i18n('missedOrDeclinedOutgoingAudioCall');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CallingNotification = (props: Props): JSX.Element | null => {
|
export const CallingNotification = (props: Props): JSX.Element | null => {
|
||||||
const { callHistoryDetails, i18n } = props;
|
const { callHistoryDetails, i18n } = props;
|
||||||
|
@ -81,7 +74,7 @@ export const CallingNotification = (props: Props): JSX.Element | null => {
|
||||||
<Timestamp
|
<Timestamp
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
timestamp={acceptedTime || endedTime}
|
timestamp={acceptedTime || endedTime}
|
||||||
extended={true}
|
extended
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
withImageNoCaption={false}
|
withImageNoCaption={false}
|
||||||
withSticker={false}
|
withSticker={false}
|
||||||
|
|
|
@ -6,11 +6,9 @@ import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import { ContactDetail, Props } from './ContactDetail';
|
import { ContactDetail, Props } from './ContactDetail';
|
||||||
import { AddressType, ContactFormType } from '../../types/Contact';
|
import { AddressType, ContactFormType } from '../../types/Contact';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/ContactDetail', module);
|
const story = storiesOf('Components/Conversation/ContactDetail', module);
|
||||||
|
|
|
@ -72,6 +72,7 @@ function getLabelForAddress(
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContactDetail extends React.Component<Props> {
|
export class ContactDetail extends React.Component<Props> {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
public renderSendMessage({
|
public renderSendMessage({
|
||||||
hasSignalAccount,
|
hasSignalAccount,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -80,20 +81,24 @@ export class ContactDetail extends React.Component<Props> {
|
||||||
hasSignalAccount: boolean;
|
hasSignalAccount: boolean;
|
||||||
i18n: (key: string, values?: Array<string>) => string;
|
i18n: (key: string, values?: Array<string>) => string;
|
||||||
onSendMessage: () => void;
|
onSendMessage: () => void;
|
||||||
}) {
|
}): JSX.Element | null {
|
||||||
if (!hasSignalAccount) {
|
if (!hasSignalAccount) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want the overall click handler for this element to fire, so we stop
|
// We don't want the overall click handler for this element to fire, so we stop
|
||||||
// propagation before handing control to the caller's callback.
|
// propagation before handing control to the caller's callback.
|
||||||
const onClick = (e: React.MouseEvent<{}>): void => {
|
const onClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSendMessage();
|
onSendMessage();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className="module-contact-detail__send-message" onClick={onClick}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="module-contact-detail__send-message"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<div className="module-contact-detail__send-message__inner">
|
<div className="module-contact-detail__send-message__inner">
|
||||||
<div className="module-contact-detail__send-message__bubble-icon" />
|
<div className="module-contact-detail__send-message__bubble-icon" />
|
||||||
{i18n('sendMessageToContact')}
|
{i18n('sendMessageToContact')}
|
||||||
|
@ -102,9 +107,13 @@ export class ContactDetail extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderEmail(items: Array<Email> | undefined, i18n: LocalizerType) {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
public renderEmail(
|
||||||
|
items: Array<Email> | undefined,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): Array<JSX.Element> | undefined {
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.map((item: Email) => {
|
return items.map((item: Email) => {
|
||||||
|
@ -122,9 +131,13 @@ export class ContactDetail extends React.Component<Props> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderPhone(items: Array<Phone> | undefined, i18n: LocalizerType) {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
public renderPhone(
|
||||||
|
items: Array<Phone> | undefined,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): Array<JSX.Element> | null | undefined {
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.map((item: Phone) => {
|
return items.map((item: Phone) => {
|
||||||
|
@ -142,15 +155,20 @@ export class ContactDetail extends React.Component<Props> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderAddressLine(value: string | undefined) {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
public renderAddressLine(value: string | undefined): JSX.Element | undefined {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>{value}</div>;
|
return <div>{value}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderPOBox(poBox: string | undefined, i18n: LocalizerType) {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
public renderPOBox(
|
||||||
|
poBox: string | undefined,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): JSX.Element | null {
|
||||||
if (!poBox) {
|
if (!poBox) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -162,7 +180,8 @@ export class ContactDetail extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderAddressLineTwo(address: PostalAddress) {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
public renderAddressLineTwo(address: PostalAddress): JSX.Element | null {
|
||||||
if (address.city || address.region || address.postcode) {
|
if (address.city || address.region || address.postcode) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -177,13 +196,14 @@ export class ContactDetail extends React.Component<Props> {
|
||||||
public renderAddresses(
|
public renderAddresses(
|
||||||
addresses: Array<PostalAddress> | undefined,
|
addresses: Array<PostalAddress> | undefined,
|
||||||
i18n: LocalizerType
|
i18n: LocalizerType
|
||||||
) {
|
): Array<JSX.Element> | undefined {
|
||||||
if (!addresses || addresses.length === 0) {
|
if (!addresses || addresses.length === 0) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return addresses.map((address: PostalAddress, index: number) => {
|
return addresses.map((address: PostalAddress, index: number) => {
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<div key={index} className="module-contact-detail__additional-contact">
|
<div key={index} className="module-contact-detail__additional-contact">
|
||||||
<div className="module-contact-detail__additional-contact__type">
|
<div className="module-contact-detail__additional-contact__type">
|
||||||
{getLabelForAddress(address, i18n)}
|
{getLabelForAddress(address, i18n)}
|
||||||
|
@ -198,7 +218,7 @@ export class ContactDetail extends React.Component<Props> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { contact, hasSignalAccount, i18n, onSendMessage } = this.props;
|
const { contact, hasSignalAccount, i18n, onSendMessage } = this.props;
|
||||||
const isIncoming = false;
|
const isIncoming = false;
|
||||||
const module = 'contact-detail';
|
const module = 'contact-detail';
|
||||||
|
|
|
@ -2,11 +2,8 @@ import * as React from 'react';
|
||||||
|
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import enMessages from '../../../\_locales/en/messages.json';
|
|
||||||
|
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
|
@ -12,15 +12,12 @@ export interface PropsType {
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContactName extends React.Component<PropsType> {
|
export const ContactName = ({ module, title }: PropsType): JSX.Element => {
|
||||||
public render() {
|
const prefix = module || 'module-contact-name';
|
||||||
const { module, title } = this.props;
|
|
||||||
const prefix = module ? module : 'module-contact-name';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={prefix} dir="auto">
|
<span className={prefix} dir="auto">
|
||||||
<Emojify text={title || ''} />
|
<Emojify text={title || ''} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -3,18 +3,14 @@ import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import enMessages from '../../../\_locales/en/messages.json';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConversationHeader,
|
ConversationHeader,
|
||||||
PropsActionsType,
|
PropsActionsType,
|
||||||
PropsHousekeepingType,
|
PropsHousekeepingType,
|
||||||
PropsType,
|
PropsType,
|
||||||
} from './ConversationHeader';
|
} from './ConversationHeader';
|
||||||
|
|
||||||
import { gifUrl } from '../../storybook/Fixtures';
|
import { gifUrl } from '../../storybook/Fixtures';
|
||||||
|
|
||||||
const book = storiesOf('Components/Conversation/ConversationHeader', module);
|
const book = storiesOf('Components/Conversation/ConversationHeader', module);
|
||||||
|
|
|
@ -71,6 +71,9 @@ export type PropsType = PropsDataType &
|
||||||
|
|
||||||
export class ConversationHeader extends React.Component<PropsType> {
|
export class ConversationHeader extends React.Component<PropsType> {
|
||||||
public showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
public showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
|
||||||
|
// Comes from a third-party dependency
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public menuTriggerRef: React.RefObject<any>;
|
public menuTriggerRef: React.RefObject<any>;
|
||||||
|
|
||||||
public constructor(props: PropsType) {
|
public constructor(props: PropsType) {
|
||||||
|
@ -80,28 +83,30 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
this.showMenuBound = this.showMenu.bind(this);
|
this.showMenuBound = this.showMenu.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public showMenu(event: React.MouseEvent<HTMLButtonElement>) {
|
public showMenu(event: React.MouseEvent<HTMLButtonElement>): void {
|
||||||
if (this.menuTriggerRef.current) {
|
if (this.menuTriggerRef.current) {
|
||||||
this.menuTriggerRef.current.handleContextClick(event);
|
this.menuTriggerRef.current.handleContextClick(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderBackButton() {
|
public renderBackButton(): JSX.Element {
|
||||||
const { onGoBack, showBackButton } = this.props;
|
const { i18n, onGoBack, showBackButton } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onGoBack}
|
onClick={onGoBack}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-conversation-header__back-icon',
|
'module-conversation-header__back-icon',
|
||||||
showBackButton ? 'module-conversation-header__back-icon--show' : null
|
showBackButton ? 'module-conversation-header__back-icon--show' : null
|
||||||
)}
|
)}
|
||||||
disabled={!showBackButton}
|
disabled={!showBackButton}
|
||||||
|
aria-label={i18n('goBack')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderTitle() {
|
public renderTitle(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
|
@ -145,7 +150,7 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderAvatar() {
|
public renderAvatar(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
avatarPath,
|
avatarPath,
|
||||||
color,
|
color,
|
||||||
|
@ -176,7 +181,7 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderExpirationLength() {
|
public renderExpirationLength(): JSX.Element | null {
|
||||||
const { expirationSettingName, showBackButton } = this.props;
|
const { expirationSettingName, showBackButton } = this.props;
|
||||||
|
|
||||||
if (!expirationSettingName) {
|
if (!expirationSettingName) {
|
||||||
|
@ -200,12 +205,13 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderMoreButton(triggerId: string) {
|
public renderMoreButton(triggerId: string): JSX.Element {
|
||||||
const { showBackButton } = this.props;
|
const { i18n, showBackButton } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenuTrigger id={triggerId} ref={this.menuTriggerRef}>
|
<ContextMenuTrigger id={triggerId} ref={this.menuTriggerRef}>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={this.showMenuBound}
|
onClick={this.showMenuBound}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-conversation-header__more-button',
|
'module-conversation-header__more-button',
|
||||||
|
@ -214,16 +220,18 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
: 'module-conversation-header__more-button--show'
|
: 'module-conversation-header__more-button--show'
|
||||||
)}
|
)}
|
||||||
disabled={showBackButton}
|
disabled={showBackButton}
|
||||||
|
aria-label={i18n('moreInfo')}
|
||||||
/>
|
/>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderSearchButton() {
|
public renderSearchButton(): JSX.Element {
|
||||||
const { onSearchInConversation, showBackButton } = this.props;
|
const { i18n, onSearchInConversation, showBackButton } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onSearchInConversation}
|
onClick={onSearchInConversation}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-conversation-header__search-button',
|
'module-conversation-header__search-button',
|
||||||
|
@ -232,22 +240,31 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
: 'module-conversation-header__search-button--show'
|
: 'module-conversation-header__search-button--show'
|
||||||
)}
|
)}
|
||||||
disabled={showBackButton}
|
disabled={showBackButton}
|
||||||
|
aria-label={i18n('search')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderOutgoingAudioCallButton() {
|
public renderOutgoingAudioCallButton(): JSX.Element | null {
|
||||||
if (!window.CALLING) {
|
if (!window.CALLING) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (this.props.type === 'group' || this.props.isMe) {
|
|
||||||
|
const {
|
||||||
|
i18n,
|
||||||
|
isMe,
|
||||||
|
onOutgoingAudioCallInConversation,
|
||||||
|
showBackButton,
|
||||||
|
type,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (type === 'group' || isMe) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { onOutgoingAudioCallInConversation, showBackButton } = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onOutgoingAudioCallInConversation}
|
onClick={onOutgoingAudioCallInConversation}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-conversation-header__audio-calling-button',
|
'module-conversation-header__audio-calling-button',
|
||||||
|
@ -256,15 +273,19 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
: 'module-conversation-header__audio-calling-button--show'
|
: 'module-conversation-header__audio-calling-button--show'
|
||||||
)}
|
)}
|
||||||
disabled={showBackButton}
|
disabled={showBackButton}
|
||||||
|
aria-label={i18n('makeOutgoingCall')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderOutgoingVideoCallButton() {
|
public renderOutgoingVideoCallButton(): JSX.Element | null {
|
||||||
if (!window.CALLING) {
|
if (!window.CALLING) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (this.props.type === 'group' || this.props.isMe) {
|
|
||||||
|
const { i18n, isMe, type } = this.props;
|
||||||
|
|
||||||
|
if (type === 'group' || isMe) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,6 +293,7 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onOutgoingVideoCallInConversation}
|
onClick={onOutgoingVideoCallInConversation}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-conversation-header__video-calling-button',
|
'module-conversation-header__video-calling-button',
|
||||||
|
@ -280,11 +302,12 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
: 'module-conversation-header__video-calling-button--show'
|
: 'module-conversation-header__video-calling-button--show'
|
||||||
)}
|
)}
|
||||||
disabled={showBackButton}
|
disabled={showBackButton}
|
||||||
|
aria-label={i18n('makeOutgoingVideoCall')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderMenu(triggerId: string) {
|
public renderMenu(triggerId: string): JSX.Element {
|
||||||
const {
|
const {
|
||||||
disableTimerChanges,
|
disableTimerChanges,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -323,7 +346,9 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
}
|
}
|
||||||
muteOptions.push(...getMuteOptions(i18n));
|
muteOptions.push(...getMuteOptions(i18n));
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const disappearingTitle = i18n('disappearingMessages') as any;
|
const disappearingTitle = i18n('disappearingMessages') as any;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const muteTitle = i18n('muteNotificationsTitle') as any;
|
const muteTitle = i18n('muteNotificationsTitle') as any;
|
||||||
const isGroup = type === 'group';
|
const isGroup = type === 'group';
|
||||||
|
|
||||||
|
@ -382,7 +407,7 @@ export class ConversationHeader extends React.Component<PropsType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { id } = this.props;
|
const { id } = this.props;
|
||||||
const triggerId = `conversation-${id}`;
|
const triggerId = `conversation-${id}`;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { number as numberKnob, text } from '@storybook/addon-knobs';
|
import { number as numberKnob, text } from '@storybook/addon-knobs';
|
||||||
import { ConversationHero } from './ConversationHero';
|
|
||||||
|
|
||||||
// @ts-ignore
|
import { ConversationHero } from './ConversationHero';
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
@ -187,7 +185,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
||||||
<div style={{ width: '480px' }}>
|
<div style={{ width: '480px' }}>
|
||||||
<ConversationHero
|
<ConversationHero
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isMe={true}
|
isMe
|
||||||
title={getTitle()}
|
title={getTitle()}
|
||||||
conversationType="direct"
|
conversationType="direct"
|
||||||
phoneNumber={getPhoneNumber()}
|
phoneNumber={getPhoneNumber()}
|
||||||
|
|
|
@ -35,6 +35,8 @@ const renderMembershipRow = ({
|
||||||
sharedGroupNames.length > 0
|
sharedGroupNames.length > 0
|
||||||
) {
|
) {
|
||||||
const firstThreeGroups = take(sharedGroupNames, 3).map((group, i) => (
|
const firstThreeGroups = take(sharedGroupNames, 3).map((group, i) => (
|
||||||
|
// We cannot guarantee uniqueness of group names
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<strong key={i} className={nameClassName}>
|
<strong key={i} className={nameClassName}>
|
||||||
<Emojify text={group} />
|
<Emojify text={group} />
|
||||||
</strong>
|
</strong>
|
||||||
|
@ -56,7 +58,8 @@ const renderMembershipRow = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (firstThreeGroups.length === 3) {
|
}
|
||||||
|
if (firstThreeGroups.length === 3) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Intl
|
<Intl
|
||||||
|
@ -70,7 +73,8 @@ const renderMembershipRow = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (firstThreeGroups.length >= 2) {
|
}
|
||||||
|
if (firstThreeGroups.length >= 2) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Intl
|
<Intl
|
||||||
|
@ -83,7 +87,8 @@ const renderMembershipRow = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (firstThreeGroups.length >= 1) {
|
}
|
||||||
|
if (firstThreeGroups.length >= 1) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Intl
|
<Intl
|
||||||
|
@ -115,9 +120,11 @@ export const ConversationHero = ({
|
||||||
title,
|
title,
|
||||||
onHeightChange,
|
onHeightChange,
|
||||||
updateSharedGroups,
|
updateSharedGroups,
|
||||||
}: Props) => {
|
}: Props): JSX.Element => {
|
||||||
const firstRenderRef = React.useRef(true);
|
const firstRenderRef = React.useRef(true);
|
||||||
|
|
||||||
|
// TODO: DESKTOP-686
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// If any of the depenencies for this hook change then the height of this
|
// If any of the depenencies for this hook change then the height of this
|
||||||
// component may have changed. The cleanup function notifies listeners of
|
// component may have changed. The cleanup function notifies listeners of
|
||||||
|
@ -144,11 +151,13 @@ export const ConversationHero = ({
|
||||||
`pn-${profileName}`,
|
`pn-${profileName}`,
|
||||||
sharedGroupNames.map(g => `g-${g}`).join(' '),
|
sharedGroupNames.map(g => `g-${g}`).join(' '),
|
||||||
]);
|
]);
|
||||||
|
/* eslint-enable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
const phoneNumberOnly = Boolean(
|
const phoneNumberOnly = Boolean(
|
||||||
!name && !profileName && conversationType === 'direct'
|
!name && !profileName && conversationType === 'direct'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
return (
|
return (
|
||||||
<div className="module-conversation-hero">
|
<div className="module-conversation-hero">
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -190,4 +199,5 @@ export const ConversationHero = ({
|
||||||
{renderMembershipRow({ isMe, sharedGroupNames, conversationType, i18n })}
|
{renderMembershipRow({ isMe, sharedGroupNames, conversationType, i18n })}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
/* eslint-enable no-nested-ternary */
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,12 +5,10 @@ import { boolean, number } from '@storybook/addon-knobs';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import { EmbeddedContact, Props } from './EmbeddedContact';
|
import { EmbeddedContact, Props } from './EmbeddedContact';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import { ContactFormType } from '../../types/Contact';
|
import { ContactFormType } from '../../types/Contact';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/EmbeddedContact', module);
|
const story = storiesOf('Components/Conversation/EmbeddedContact', module);
|
||||||
|
|
|
@ -21,7 +21,7 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EmbeddedContact extends React.Component<Props> {
|
export class EmbeddedContact extends React.Component<Props> {
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
contact,
|
contact,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -36,6 +36,7 @@ export class EmbeddedContact extends React.Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-embedded-contact',
|
'module-embedded-contact',
|
||||||
`module-embedded-contact--${direction}`,
|
`module-embedded-contact--${direction}`,
|
||||||
|
|
|
@ -13,7 +13,7 @@ function getImageTag({
|
||||||
sizeClass,
|
sizeClass,
|
||||||
key,
|
key,
|
||||||
}: {
|
}: {
|
||||||
match: any;
|
match: RegExpExecArray;
|
||||||
sizeClass?: SizeClassType;
|
sizeClass?: SizeClassType;
|
||||||
key: string | number;
|
key: string | number;
|
||||||
}) {
|
}) {
|
||||||
|
@ -24,7 +24,6 @@ function getImageTag({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// tslint:disable-next-line react-a11y-img-has-alt
|
|
||||||
<img
|
<img
|
||||||
key={key}
|
key={key}
|
||||||
src={img}
|
src={img}
|
||||||
|
@ -48,15 +47,20 @@ export class Emojify extends React.Component<Props> {
|
||||||
renderNonEmoji: ({ text }) => text,
|
renderNonEmoji: ({ text }) => text,
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render():
|
||||||
|
| JSX.Element
|
||||||
|
| string
|
||||||
|
| null
|
||||||
|
| Array<JSX.Element | string | null> {
|
||||||
const { text, sizeClass, renderNonEmoji } = this.props;
|
const { text, sizeClass, renderNonEmoji } = this.props;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const results: Array<any> = [];
|
const results: Array<any> = [];
|
||||||
const regex = emojiRegex();
|
const regex = emojiRegex();
|
||||||
|
|
||||||
// We have to do this, because renderNonEmoji is not required in our Props object,
|
// We have to do this, because renderNonEmoji is not required in our Props object,
|
||||||
// but it is always provided via defaultProps.
|
// but it is always provided via defaultProps.
|
||||||
if (!renderNonEmoji) {
|
if (!renderNonEmoji) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let match = regex.exec(text);
|
let match = regex.exec(text);
|
||||||
|
@ -70,17 +74,20 @@ export class Emojify extends React.Component<Props> {
|
||||||
while (match) {
|
while (match) {
|
||||||
if (last < match.index) {
|
if (last < match.index) {
|
||||||
const textWithNoEmoji = text.slice(last, match.index);
|
const textWithNoEmoji = text.slice(last, match.index);
|
||||||
results.push(renderNonEmoji({ text: textWithNoEmoji, key: count++ }));
|
count += 1;
|
||||||
|
results.push(renderNonEmoji({ text: textWithNoEmoji, key: count }));
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push(getImageTag({ match, sizeClass, key: count++ }));
|
count += 1;
|
||||||
|
results.push(getImageTag({ match, sizeClass, key: count }));
|
||||||
|
|
||||||
last = regex.lastIndex;
|
last = regex.lastIndex;
|
||||||
match = regex.exec(text);
|
match = regex.exec(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last < text.length) {
|
if (last < text.length) {
|
||||||
results.push(renderNonEmoji({ text: text.slice(last), key: count++ }));
|
count += 1;
|
||||||
|
results.push(renderNonEmoji({ text: text.slice(last), key: count }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
|
@ -13,7 +13,7 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpireTimer extends React.Component<Props> {
|
export class ExpireTimer extends React.Component<Props> {
|
||||||
private interval: any;
|
private interval: NodeJS.Timeout | null;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -21,26 +21,28 @@ export class ExpireTimer extends React.Component<Props> {
|
||||||
this.interval = null;
|
this.interval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount(): void {
|
||||||
const { expirationLength } = this.props;
|
const { expirationLength } = this.props;
|
||||||
const increment = getIncrement(expirationLength);
|
const increment = getIncrement(expirationLength);
|
||||||
const updateFrequency = Math.max(increment, 500);
|
const updateFrequency = Math.max(increment, 500);
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
// Used to trigger renders
|
||||||
|
// eslint-disable-next-line react/no-unused-state
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
this.interval = setInterval(update, updateFrequency);
|
this.interval = setInterval(update, updateFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
direction,
|
direction,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
|
|
|
@ -33,7 +33,10 @@ type PropsHousekeeping = {
|
||||||
export type Props = PropsData & PropsHousekeeping;
|
export type Props = PropsData & PropsHousekeeping;
|
||||||
|
|
||||||
export class GroupNotification extends React.Component<Props> {
|
export class GroupNotification extends React.Component<Props> {
|
||||||
public renderChange(change: Change, from: Contact) {
|
public renderChange(
|
||||||
|
change: Change,
|
||||||
|
from: Contact
|
||||||
|
): JSX.Element | string | null | undefined {
|
||||||
const { contacts, type, newName } = change;
|
const { contacts, type, newName } = change;
|
||||||
const { i18n } = this.props;
|
const { i18n } = this.props;
|
||||||
|
|
||||||
|
@ -78,6 +81,7 @@ export class GroupNotification extends React.Component<Props> {
|
||||||
throw new Error('Group update is missing contacts');
|
throw new Error('Group update is missing contacts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
const otherPeopleNotifMsg =
|
const otherPeopleNotifMsg =
|
||||||
otherPeople.length === 1
|
otherPeople.length === 1
|
||||||
? 'joinedTheGroup'
|
? 'joinedTheGroup'
|
||||||
|
@ -108,6 +112,7 @@ export class GroupNotification extends React.Component<Props> {
|
||||||
throw new Error('Group update is missing contacts');
|
throw new Error('Group update is missing contacts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
const leftKey =
|
const leftKey =
|
||||||
contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
|
contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
|
||||||
|
|
||||||
|
@ -115,13 +120,14 @@ export class GroupNotification extends React.Component<Props> {
|
||||||
<Intl i18n={i18n} id={leftKey} components={[otherPeopleWithCommas]} />
|
<Intl i18n={i18n} id={leftKey} components={[otherPeopleWithCommas]} />
|
||||||
);
|
);
|
||||||
case 'general':
|
case 'general':
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(type);
|
throw missingCaseError(type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { changes, i18n, from } = this.props;
|
const { changes, i18n, from } = this.props;
|
||||||
|
|
||||||
// Leave messages are always from the person leaving, so we omit the fromLabel if
|
// Leave messages are always from the person leaving, so we omit the fromLabel if
|
||||||
|
@ -153,8 +159,9 @@ export class GroupNotification extends React.Component<Props> {
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(changes || []).map((change, index) => (
|
{(changes || []).map((change, i) => (
|
||||||
<div key={index} className="module-group-notification__change">
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<div key={i} className="module-group-notification__change">
|
||||||
{this.renderChange(change, from)}
|
{this.renderChange(change, from)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
|
/* eslint-disable-next-line max-classes-per-file */
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
|
||||||
|
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import { GroupV2ChangeType } from '../../groups';
|
import { GroupV2ChangeType } from '../../groups';
|
||||||
import { SmartContactRendererType } from '../../groupChange';
|
import { SmartContactRendererType } from '../../groupChange';
|
||||||
import { GroupV2Change } from './GroupV2Change';
|
import { GroupV2Change } from './GroupV2Change';
|
||||||
|
@ -19,17 +17,21 @@ const CONTACT_C = 'CONTACT_C';
|
||||||
const ADMIN_A = 'ADMIN_A';
|
const ADMIN_A = 'ADMIN_A';
|
||||||
const INVITEE_A = 'INVITEE_A';
|
const INVITEE_A = 'INVITEE_A';
|
||||||
|
|
||||||
// tslint:disable-next-line no-unnecessary-class
|
|
||||||
class AccessControlEnum {
|
class AccessControlEnum {
|
||||||
static UNKNOWN = 0;
|
static UNKNOWN = 0;
|
||||||
|
|
||||||
static ADMINISTRATOR = 1;
|
static ADMINISTRATOR = 1;
|
||||||
|
|
||||||
static ANY = 2;
|
static ANY = 2;
|
||||||
|
|
||||||
static MEMBER = 3;
|
static MEMBER = 3;
|
||||||
}
|
}
|
||||||
// tslint:disable-next-line no-unnecessary-class
|
|
||||||
class RoleEnum {
|
class RoleEnum {
|
||||||
static UNKNOWN = 0;
|
static UNKNOWN = 0;
|
||||||
|
|
||||||
static ADMINISTRATOR = 1;
|
static ADMINISTRATOR = 1;
|
||||||
|
|
||||||
static DEFAULT = 2;
|
static DEFAULT = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -468,7 +470,6 @@ storiesOf('Components/Conversation/GroupV2Change', module)
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
// tslint:disable-next-line max-func-body-length
|
|
||||||
.add('Member Privilege', () => {
|
.add('Member Privilege', () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -652,7 +653,6 @@ storiesOf('Components/Conversation/GroupV2Change', module)
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
// tslint:disable-next-line max-func-body-length
|
|
||||||
.add('Pending Remove - one', () => {
|
.add('Pending Remove - one', () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -53,6 +53,8 @@ export function GroupV2Change(props: PropsType): React.ReactElement {
|
||||||
renderString: renderStringToIntl,
|
renderString: renderStringToIntl,
|
||||||
RoleEnum,
|
RoleEnum,
|
||||||
}).map((item: FullJSXType, index: number) => (
|
}).map((item: FullJSXType, index: number) => (
|
||||||
|
// Difficult to find a unique key for this type
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<div key={index}>{item}</div>
|
<div key={index}>{item}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,16 +7,13 @@ import { storiesOf } from '@storybook/react';
|
||||||
import { pngUrl } from '../../storybook/Fixtures';
|
import { pngUrl } from '../../storybook/Fixtures';
|
||||||
import { Image, Props } from './Image';
|
import { Image, Props } from './Image';
|
||||||
import { IMAGE_PNG } from '../../types/MIME';
|
import { IMAGE_PNG } from '../../types/MIME';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/Image', module);
|
const story = storiesOf('Components/Conversation/Image', module);
|
||||||
|
|
||||||
// tslint:disable-next-line:cyclomatic-complexity
|
|
||||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
alt: text('alt', overrideProps.alt || ''),
|
alt: text('alt', overrideProps.alt || ''),
|
||||||
attachment: overrideProps.attachment || {
|
attachment: overrideProps.attachment || {
|
||||||
|
@ -170,6 +167,7 @@ story.add('Blurhash', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
blurHash: 'thisisafakeblurhashthatwasmadeup',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
url: undefined as any,
|
url: undefined as any,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -179,7 +177,9 @@ story.add('Missing Image', () => {
|
||||||
const defaultProps = createProps();
|
const defaultProps = createProps();
|
||||||
const props = {
|
const props = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
attachment: undefined as any,
|
attachment: undefined as any,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
url: undefined as any,
|
url: undefined as any,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ export class Image extends React.Component<Props> {
|
||||||
return Boolean(onClick && !pending && url);
|
return Boolean(onClick && !pending && url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleClick = (event: React.MouseEvent) => {
|
public handleClick = (event: React.MouseEvent): void => {
|
||||||
if (!this.canClick()) {
|
if (!this.canClick()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -65,7 +65,9 @@ export class Image extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
public handleKeyDown = (
|
||||||
|
event: React.KeyboardEvent<HTMLButtonElement>
|
||||||
|
): void => {
|
||||||
if (!this.canClick()) {
|
if (!this.canClick()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -82,8 +84,7 @@ export class Image extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
public render(): JSX.Element {
|
||||||
public render() {
|
|
||||||
const {
|
const {
|
||||||
alt,
|
alt,
|
||||||
attachment,
|
attachment,
|
||||||
|
@ -127,7 +128,10 @@ export class Image extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
|
|
||||||
const overlay = canClick ? (
|
const overlay = canClick ? (
|
||||||
|
// Not sure what this button does.
|
||||||
|
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={overlayClassName}
|
className={overlayClassName}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
@ -135,6 +139,7 @@ export class Image extends React.Component<Props> {
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -210,7 +215,8 @@ export class Image extends React.Component<Props> {
|
||||||
{overlay}
|
{overlay}
|
||||||
{closeButton ? (
|
{closeButton ? (
|
||||||
<button
|
<button
|
||||||
onClick={(e: React.MouseEvent<{}>) => {
|
type="button"
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -220,9 +226,11 @@ export class Image extends React.Component<Props> {
|
||||||
}}
|
}}
|
||||||
className="module-image__close-button"
|
className="module-image__close-button"
|
||||||
title={i18n('remove-attachment')}
|
title={i18n('remove-attachment')}
|
||||||
|
aria-label={i18n('remove-attachment')}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
/* eslint-enable no-nested-ternary */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,10 @@ import {
|
||||||
MIMEType,
|
MIMEType,
|
||||||
VIDEO_MP4,
|
VIDEO_MP4,
|
||||||
} from '../../types/MIME';
|
} from '../../types/MIME';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import { pngUrl, squareStickerUrl } from '../../storybook/Fixtures';
|
import { pngUrl, squareStickerUrl } from '../../storybook/Fixtures';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/ImageGrid', module);
|
const story = storiesOf('Components/Conversation/ImageGrid', module);
|
||||||
|
|
|
@ -30,10 +30,7 @@ export interface Props {
|
||||||
onClick?: (attachment: AttachmentType) => void;
|
onClick?: (attachment: AttachmentType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageGrid extends React.Component<Props> {
|
export const ImageGrid = ({
|
||||||
// tslint:disable-next-line max-func-body-length */
|
|
||||||
public render() {
|
|
||||||
const {
|
|
||||||
attachments,
|
attachments,
|
||||||
bottomOverlay,
|
bottomOverlay,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -44,12 +41,11 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
tabIndex,
|
tabIndex,
|
||||||
withContentAbove,
|
withContentAbove,
|
||||||
withContentBelow,
|
withContentBelow,
|
||||||
} = this.props;
|
}: Props): JSX.Element | null => {
|
||||||
|
const curveTopLeft = !withContentAbove;
|
||||||
const curveTopLeft = !Boolean(withContentAbove);
|
|
||||||
const curveTopRight = curveTopLeft;
|
const curveTopRight = curveTopLeft;
|
||||||
|
|
||||||
const curveBottom = !Boolean(withContentBelow);
|
const curveBottom = !withContentBelow;
|
||||||
const curveBottomLeft = curveBottom;
|
const curveBottomLeft = curveBottom;
|
||||||
const curveBottomRight = curveBottom;
|
const curveBottomRight = curveBottom;
|
||||||
|
|
||||||
|
@ -347,5 +343,4 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ export type PropsType = {
|
||||||
export class InlineNotificationWrapper extends React.Component<PropsType> {
|
export class InlineNotificationWrapper extends React.Component<PropsType> {
|
||||||
public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
|
public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||||
|
|
||||||
public setFocus = () => {
|
public setFocus = (): void => {
|
||||||
const container = this.focusRef.current;
|
const container = this.focusRef.current;
|
||||||
|
|
||||||
if (container && !container.contains(document.activeElement)) {
|
if (container && !container.contains(document.activeElement)) {
|
||||||
|
@ -18,14 +18,15 @@ export class InlineNotificationWrapper extends React.Component<PropsType> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleFocus = () => {
|
public handleFocus = (): void => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (window.getInteractionMode() === 'keyboard') {
|
if (window.getInteractionMode() === 'keyboard') {
|
||||||
this.setSelected();
|
this.setSelected();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public setSelected = () => {
|
public setSelected = (): void => {
|
||||||
const { id, conversationId, selectMessage } = this.props;
|
const { id, conversationId, selectMessage } = this.props;
|
||||||
|
|
||||||
if (selectMessage) {
|
if (selectMessage) {
|
||||||
|
@ -33,25 +34,28 @@ export class InlineNotificationWrapper extends React.Component<PropsType> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount(): void {
|
||||||
const { isSelected } = this.props;
|
const { isSelected } = this.props;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: PropsType) {
|
public componentDidUpdate(prevProps: PropsType): void {
|
||||||
if (!prevProps.isSelected && this.props.isSelected) {
|
const { isSelected } = this.props;
|
||||||
|
|
||||||
|
if (!prevProps.isSelected && isSelected) {
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="module-inline-notification-wrapper"
|
className="module-inline-notification-wrapper"
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ref={this.focusRef}
|
ref={this.focusRef}
|
||||||
onFocus={this.handleFocus}
|
onFocus={this.handleFocus}
|
||||||
|
|
|
@ -4,11 +4,9 @@ import { number } from '@storybook/addon-knobs';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import { LastSeenIndicator, Props } from './LastSeenIndicator';
|
import { LastSeenIndicator, Props } from './LastSeenIndicator';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/LastSeenIndicator', module);
|
const story = storiesOf('Components/Conversation/LastSeenIndicator', module);
|
||||||
|
|
|
@ -7,10 +7,7 @@ export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class LastSeenIndicator extends React.Component<Props> {
|
export const LastSeenIndicator = ({ count, i18n }: Props): JSX.Element => {
|
||||||
public render() {
|
|
||||||
const { count, i18n } = this.props;
|
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
count === 1
|
count === 1
|
||||||
? i18n('unreadMessage')
|
? i18n('unreadMessage')
|
||||||
|
@ -22,5 +19,4 @@ export class LastSeenIndicator extends React.Component<Props> {
|
||||||
<div className="module-last-seen-indicator__text">{message}</div>
|
<div className="module-last-seen-indicator__text">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -20,17 +20,21 @@ export class Linkify extends React.Component<Props> {
|
||||||
renderNonLink: ({ text }) => text,
|
renderNonLink: ({ text }) => text,
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render():
|
||||||
|
| JSX.Element
|
||||||
|
| string
|
||||||
|
| null
|
||||||
|
| Array<JSX.Element | string | null> {
|
||||||
const { text, renderNonLink } = this.props;
|
const { text, renderNonLink } = this.props;
|
||||||
const matchData = linkify.match(text) || [];
|
const matchData = linkify.match(text) || [];
|
||||||
const results: Array<any> = [];
|
const results: Array<JSX.Element | string> = [];
|
||||||
let last = 0;
|
let last = 0;
|
||||||
let count = 1;
|
let count = 1;
|
||||||
|
|
||||||
// We have to do this, because renderNonLink is not required in our Props object,
|
// We have to do this, because renderNonLink is not required in our Props object,
|
||||||
// but it is always provided via defaultProps.
|
// but it is always provided via defaultProps.
|
||||||
if (!renderNonLink) {
|
if (!renderNonLink) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchData.length === 0) {
|
if (matchData.length === 0) {
|
||||||
|
@ -46,18 +50,20 @@ export class Linkify extends React.Component<Props> {
|
||||||
}) => {
|
}) => {
|
||||||
if (last < match.index) {
|
if (last < match.index) {
|
||||||
const textWithNoLink = text.slice(last, match.index);
|
const textWithNoLink = text.slice(last, match.index);
|
||||||
results.push(renderNonLink({ text: textWithNoLink, key: count++ }));
|
count += 1;
|
||||||
|
results.push(renderNonLink({ text: textWithNoLink, key: count }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { url, text: originalText } = match;
|
const { url, text: originalText } = match;
|
||||||
|
count += 1;
|
||||||
if (SUPPORTED_PROTOCOLS.test(url) && !isLinkSneaky(url)) {
|
if (SUPPORTED_PROTOCOLS.test(url) && !isLinkSneaky(url)) {
|
||||||
results.push(
|
results.push(
|
||||||
<a key={count++} href={url}>
|
<a key={count} href={url}>
|
||||||
{originalText}
|
{originalText}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
results.push(renderNonLink({ text: originalText, key: count++ }));
|
results.push(renderNonLink({ text: originalText, key: count }));
|
||||||
}
|
}
|
||||||
|
|
||||||
last = match.lastIndex;
|
last = match.lastIndex;
|
||||||
|
@ -65,7 +71,8 @@ export class Linkify extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (last < text.length) {
|
if (last < text.length) {
|
||||||
results.push(renderNonLink({ text: text.slice(last), key: count++ }));
|
count += 1;
|
||||||
|
results.push(renderNonLink({ text: text.slice(last), key: count }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
|
@ -15,12 +15,10 @@ import {
|
||||||
MIMEType,
|
MIMEType,
|
||||||
VIDEO_MP4,
|
VIDEO_MP4,
|
||||||
} from '../../types/MIME';
|
} from '../../types/MIME';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import { pngUrl } from '../../storybook/Fixtures';
|
import { pngUrl } from '../../storybook/Fixtures';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/Message', module);
|
const story = storiesOf('Components/Conversation/Message', module);
|
||||||
|
@ -75,7 +73,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
previews: overrideProps.previews || [],
|
previews: overrideProps.previews || [],
|
||||||
reactions: overrideProps.reactions,
|
reactions: overrideProps.reactions,
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
renderEmojiPicker: renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
replyToMessage: action('replyToMessage'),
|
replyToMessage: action('replyToMessage'),
|
||||||
retrySend: action('retrySend'),
|
retrySend: action('retrySend'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
|
@ -195,7 +193,6 @@ story.add('Older', () => {
|
||||||
return renderBothDirections(props);
|
return renderBothDirections(props);
|
||||||
});
|
});
|
||||||
|
|
||||||
// tslint:disable-next-line:max-func-body-length
|
|
||||||
story.add('Reactions', () => {
|
story.add('Reactions', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
text: 'Hello there from a pal!',
|
text: 'Hello there from a pal!',
|
||||||
|
|
|
@ -3,6 +3,7 @@ import ReactDOM, { createPortal } from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Measure from 'react-measure';
|
import Measure from 'react-measure';
|
||||||
import { drop, groupBy, orderBy, take } from 'lodash';
|
import { drop, groupBy, orderBy, take } from 'lodash';
|
||||||
|
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
import moment, { Moment } from 'moment';
|
import moment, { Moment } from 'moment';
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker';
|
||||||
import { Emoji } from '../emoji/Emoji';
|
import { Emoji } from '../emoji/Emoji';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AttachmentType,
|
||||||
canDisplayImage,
|
canDisplayImage,
|
||||||
getExtensionForDisplay,
|
getExtensionForDisplay,
|
||||||
getGridDimensions,
|
getGridDimensions,
|
||||||
|
@ -34,8 +36,7 @@ import {
|
||||||
isImage,
|
isImage,
|
||||||
isImageAttachment,
|
isImageAttachment,
|
||||||
isVideo,
|
isVideo,
|
||||||
} from '../../../ts/types/Attachment';
|
} from '../../types/Attachment';
|
||||||
import { AttachmentType } from '../../types/Attachment';
|
|
||||||
import { ContactType } from '../../types/Contact';
|
import { ContactType } from '../../types/Contact';
|
||||||
|
|
||||||
import { getIncrement } from '../../util/timer';
|
import { getIncrement } from '../../util/timer';
|
||||||
|
@ -43,7 +44,6 @@ import { isFileDangerous } from '../../util/isFileDangerous';
|
||||||
import { BodyRangesType, LocalizerType } from '../../types/Util';
|
import { BodyRangesType, LocalizerType } from '../../types/Util';
|
||||||
import { ColorType } from '../../types/Colors';
|
import { ColorType } from '../../types/Colors';
|
||||||
import { createRefMerger } from '../_util';
|
import { createRefMerger } from '../_util';
|
||||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
|
||||||
|
|
||||||
interface Trigger {
|
interface Trigger {
|
||||||
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
@ -209,18 +209,24 @@ const EXPIRED_DELAY = 600;
|
||||||
|
|
||||||
export class Message extends React.PureComponent<Props, State> {
|
export class Message extends React.PureComponent<Props, State> {
|
||||||
public menuTriggerRef: Trigger | undefined;
|
public menuTriggerRef: Trigger | undefined;
|
||||||
|
|
||||||
public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();
|
public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();
|
||||||
|
|
||||||
public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
|
public focusRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||||
|
|
||||||
public reactionsContainerRef: React.RefObject<
|
public reactionsContainerRef: React.RefObject<
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
> = React.createRef();
|
> = React.createRef();
|
||||||
|
|
||||||
public reactionsContainerRefMerger = createRefMerger();
|
public reactionsContainerRefMerger = createRefMerger();
|
||||||
|
|
||||||
public wideMl: MediaQueryList;
|
public wideMl: MediaQueryList;
|
||||||
|
|
||||||
public expirationCheckInterval: any;
|
public expirationCheckInterval: NodeJS.Timeout | undefined;
|
||||||
public expiredTimeout: any;
|
|
||||||
public selectedTimeout: any;
|
public expiredTimeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
public selectedTimeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
public constructor(props: Props) {
|
public constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -268,24 +274,23 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleWideMlChange = (event: MediaQueryListEvent) => {
|
public handleWideMlChange = (event: MediaQueryListEvent): void => {
|
||||||
this.setState({ isWide: event.matches });
|
this.setState({ isWide: event.matches });
|
||||||
};
|
};
|
||||||
|
|
||||||
public captureMenuTrigger = (triggerRef: Trigger) => {
|
public captureMenuTrigger = (triggerRef: Trigger): void => {
|
||||||
this.menuTriggerRef = triggerRef;
|
this.menuTriggerRef = triggerRef;
|
||||||
};
|
};
|
||||||
|
|
||||||
public showMenu = (event: React.MouseEvent<HTMLDivElement>) => {
|
public showMenu = (event: React.MouseEvent<HTMLDivElement>): void => {
|
||||||
if (this.menuTriggerRef) {
|
if (this.menuTriggerRef) {
|
||||||
this.menuTriggerRef.handleContextClick(event);
|
this.menuTriggerRef.handleContextClick(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleImageError = () => {
|
public handleImageError = (): void => {
|
||||||
const { id } = this.props;
|
const { id } = this.props;
|
||||||
// tslint:disable-next-line no-console
|
window.log.info(
|
||||||
console.log(
|
|
||||||
`Message ${id}: Image failed to load; failing over to placeholder`
|
`Message ${id}: Image failed to load; failing over to placeholder`
|
||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -293,7 +298,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleFocus = () => {
|
public handleFocus = (): void => {
|
||||||
const { interactionMode } = this.props;
|
const { interactionMode } = this.props;
|
||||||
|
|
||||||
if (interactionMode === 'keyboard') {
|
if (interactionMode === 'keyboard') {
|
||||||
|
@ -301,7 +306,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public setSelected = () => {
|
public setSelected = (): void => {
|
||||||
const { id, conversationId, selectMessage } = this.props;
|
const { id, conversationId, selectMessage } = this.props;
|
||||||
|
|
||||||
if (selectMessage) {
|
if (selectMessage) {
|
||||||
|
@ -309,7 +314,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public setFocus = () => {
|
public setFocus = (): void => {
|
||||||
const container = this.focusRef.current;
|
const container = this.focusRef.current;
|
||||||
|
|
||||||
if (container && !container.contains(document.activeElement)) {
|
if (container && !container.contains(document.activeElement)) {
|
||||||
|
@ -317,7 +322,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.startSelectedTimer();
|
this.startSelectedTimer();
|
||||||
|
|
||||||
const { isSelected } = this.props;
|
const { isSelected } = this.props;
|
||||||
|
@ -340,7 +345,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}, checkFrequency);
|
}, checkFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
if (this.selectedTimeout) {
|
if (this.selectedTimeout) {
|
||||||
clearInterval(this.selectedTimeout);
|
clearInterval(this.selectedTimeout);
|
||||||
}
|
}
|
||||||
|
@ -356,18 +361,20 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
this.wideMl.removeEventListener('change', this.handleWideMlChange);
|
this.wideMl.removeEventListener('change', this.handleWideMlChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Props) {
|
public componentDidUpdate(prevProps: Props): void {
|
||||||
|
const { isSelected } = this.props;
|
||||||
|
|
||||||
this.startSelectedTimer();
|
this.startSelectedTimer();
|
||||||
|
|
||||||
if (!prevProps.isSelected && this.props.isSelected) {
|
if (!prevProps.isSelected && isSelected) {
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkExpired();
|
this.checkExpired();
|
||||||
}
|
}
|
||||||
|
|
||||||
public startSelectedTimer() {
|
public startSelectedTimer(): void {
|
||||||
const { interactionMode } = this.props;
|
const { clearSelectedMessage, interactionMode } = this.props;
|
||||||
const { isSelected } = this.state;
|
const { isSelected } = this.state;
|
||||||
|
|
||||||
if (interactionMode === 'keyboard' || !isSelected) {
|
if (interactionMode === 'keyboard' || !isSelected) {
|
||||||
|
@ -378,12 +385,12 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
this.selectedTimeout = setTimeout(() => {
|
this.selectedTimeout = setTimeout(() => {
|
||||||
this.selectedTimeout = undefined;
|
this.selectedTimeout = undefined;
|
||||||
this.setState({ isSelected: false });
|
this.setState({ isSelected: false });
|
||||||
this.props.clearSelectedMessage();
|
clearSelectedMessage();
|
||||||
}, SELECTED_TIMEOUT);
|
}, SELECTED_TIMEOUT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkExpired() {
|
public checkExpired(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const { isExpired, expirationTimestamp, expirationLength } = this.props;
|
const { isExpired, expirationTimestamp, expirationLength } = this.props;
|
||||||
|
|
||||||
|
@ -408,7 +415,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderTimestamp() {
|
public renderTimestamp(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -442,6 +449,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
i18n('sendFailed')
|
i18n('sendFailed')
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="module-message__metadata__tapable"
|
className="module-message__metadata__tapable"
|
||||||
onClick={(event: React.MouseEvent) => {
|
onClick={(event: React.MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -463,7 +471,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<Timestamp
|
<Timestamp
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
extended={true}
|
extended
|
||||||
direction={metadataDirection}
|
direction={metadataDirection}
|
||||||
withImageNoCaption={withImageNoCaption}
|
withImageNoCaption={withImageNoCaption}
|
||||||
withSticker={isSticker}
|
withSticker={isSticker}
|
||||||
|
@ -473,8 +481,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity
|
public renderMetadata(): JSX.Element | null {
|
||||||
public renderMetadata() {
|
|
||||||
const {
|
const {
|
||||||
collapseMetadata,
|
collapseMetadata,
|
||||||
direction,
|
direction,
|
||||||
|
@ -548,7 +555,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderAuthor() {
|
public renderAuthor(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
authorTitle,
|
authorTitle,
|
||||||
authorName,
|
authorName,
|
||||||
|
@ -564,7 +571,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (collapseMetadata) {
|
if (collapseMetadata) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -597,8 +604,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
public renderAttachment(): JSX.Element | null {
|
||||||
public renderAttachment() {
|
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
collapseMetadata,
|
collapseMetadata,
|
||||||
|
@ -667,11 +673,12 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (!firstAttachment.pending && isAudio(attachments)) {
|
}
|
||||||
|
if (!firstAttachment.pending && isAudio(attachments)) {
|
||||||
return (
|
return (
|
||||||
<audio
|
<audio
|
||||||
ref={this.audioRef}
|
ref={this.audioRef}
|
||||||
controls={true}
|
controls
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__audio-attachment',
|
'module-message__audio-attachment',
|
||||||
withContentBelow
|
withContentBelow
|
||||||
|
@ -686,13 +693,14 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<source src={firstAttachment.url} />
|
<source src={firstAttachment.url} />
|
||||||
</audio>
|
</audio>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
||||||
const extension = getExtensionForDisplay({ contentType, fileName });
|
const extension = getExtensionForDisplay({ contentType, fileName });
|
||||||
const isDangerous = isFileDangerous(fileName || '');
|
const isDangerous = isFileDangerous(fileName || '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__generic-attachment',
|
'module-message__generic-attachment',
|
||||||
withContentBelow
|
withContentBelow
|
||||||
|
@ -759,10 +767,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
public renderPreview(): JSX.Element | null {
|
||||||
public renderPreview() {
|
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
conversationType,
|
conversationType,
|
||||||
|
@ -809,6 +815,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__link-preview',
|
'module-message__link-preview',
|
||||||
`module-message__link-preview--${direction}`,
|
`module-message__link-preview--${direction}`,
|
||||||
|
@ -835,7 +842,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<ImageGrid
|
<ImageGrid
|
||||||
attachments={[first.image]}
|
attachments={[first.image]}
|
||||||
withContentAbove={withContentAbove}
|
withContentAbove={withContentAbove}
|
||||||
withContentBelow={true}
|
withContentBelow
|
||||||
onError={this.handleImageError}
|
onError={this.handleImageError}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
|
@ -852,9 +859,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<div className="module-message__link-preview__icon_container">
|
<div className="module-message__link-preview__icon_container">
|
||||||
<Image
|
<Image
|
||||||
smallCurveTopLeft={!withContentAbove}
|
smallCurveTopLeft={!withContentAbove}
|
||||||
noBorder={true}
|
noBorder
|
||||||
noBackground={true}
|
noBackground
|
||||||
softCorners={true}
|
softCorners
|
||||||
alt={i18n('previewThumbnail', [first.domain])}
|
alt={i18n('previewThumbnail', [first.domain])}
|
||||||
height={72}
|
height={72}
|
||||||
width={72}
|
width={72}
|
||||||
|
@ -900,7 +907,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderQuote() {
|
public renderQuote(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
conversationType,
|
conversationType,
|
||||||
authorColor,
|
authorColor,
|
||||||
|
@ -952,7 +959,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderEmbeddedContact() {
|
public renderEmbeddedContact(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
collapseMetadata,
|
collapseMetadata,
|
||||||
contact,
|
contact,
|
||||||
|
@ -989,7 +996,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderSendMessageButton() {
|
public renderSendMessageButton(): JSX.Element | null {
|
||||||
const { contact, openConversation, i18n } = this.props;
|
const { contact, openConversation, i18n } = this.props;
|
||||||
if (!contact || !contact.signalAccount) {
|
if (!contact || !contact.signalAccount) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -997,6 +1004,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (contact.signalAccount) {
|
if (contact.signalAccount) {
|
||||||
openConversation(contact.signalAccount);
|
openConversation(contact.signalAccount);
|
||||||
|
@ -1009,7 +1017,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderAvatar() {
|
public renderAvatar(): JSX.Element | undefined {
|
||||||
const {
|
const {
|
||||||
authorAvatarPath,
|
authorAvatarPath,
|
||||||
authorName,
|
authorName,
|
||||||
|
@ -1031,6 +1039,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return (
|
return (
|
||||||
<div className="module-message__author-avatar">
|
<div className="module-message__author-avatar">
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -1048,7 +1057,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderText() {
|
public renderText(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
|
@ -1060,6 +1069,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
textPending,
|
textPending,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
const contents = deletedForEveryone
|
const contents = deletedForEveryone
|
||||||
? i18n('message--deletedForEveryone')
|
? i18n('message--deletedForEveryone')
|
||||||
: direction === 'incoming' && status === 'error'
|
: direction === 'incoming' && status === 'error'
|
||||||
|
@ -1093,7 +1103,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderError(isCorrectSide: boolean) {
|
public renderError(isCorrectSide: boolean): JSX.Element | null {
|
||||||
const { status, direction } = this.props;
|
const { status, direction } = this.props;
|
||||||
|
|
||||||
if (!isCorrectSide || (status !== 'error' && status !== 'partial-sent')) {
|
if (!isCorrectSide || (status !== 'error' && status !== 'partial-sent')) {
|
||||||
|
@ -1112,10 +1122,12 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderMenu(isCorrectSide: boolean, triggerId: string) {
|
public renderMenu(
|
||||||
|
isCorrectSide: boolean,
|
||||||
|
triggerId: string
|
||||||
|
): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
// tslint:disable-next-line max-func-body-length
|
|
||||||
canReply,
|
canReply,
|
||||||
direction,
|
direction,
|
||||||
disableMenu,
|
disableMenu,
|
||||||
|
@ -1123,8 +1135,10 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
id,
|
id,
|
||||||
isSticker,
|
isSticker,
|
||||||
isTapToView,
|
isTapToView,
|
||||||
|
reactToMessage,
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
|
selectedReaction,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!isCorrectSide || disableMenu) {
|
if (!isCorrectSide || disableMenu) {
|
||||||
|
@ -1142,10 +1156,13 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
!isTapToView &&
|
!isTapToView &&
|
||||||
firstAttachment &&
|
firstAttachment &&
|
||||||
!firstAttachment.pending ? (
|
!firstAttachment.pending ? (
|
||||||
|
// This a menu meant for mouse use only
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// eslint-disable-next-line jsx-a11y/interactive-supports-focus, jsx-a11y/click-events-have-key-events
|
||||||
<div
|
<div
|
||||||
onClick={this.openGenericAttachment}
|
onClick={this.openGenericAttachment}
|
||||||
// This a menu meant for mouse use only
|
|
||||||
role="button"
|
role="button"
|
||||||
|
aria-label={i18n('downloadAttachment')}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__buttons__download',
|
'module-message__buttons__download',
|
||||||
`module-message__buttons__download--${direction}`
|
`module-message__buttons__download--${direction}`
|
||||||
|
@ -1161,6 +1178,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
const maybePopperRef = isWide ? popperRef : undefined;
|
const maybePopperRef = isWide ? popperRef : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// This a menu meant for mouse use only
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// eslint-disable-next-line jsx-a11y/interactive-supports-focus, jsx-a11y/click-events-have-key-events
|
||||||
<div
|
<div
|
||||||
ref={maybePopperRef}
|
ref={maybePopperRef}
|
||||||
onClick={(event: React.MouseEvent) => {
|
onClick={(event: React.MouseEvent) => {
|
||||||
|
@ -1171,6 +1191,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
className="module-message__buttons__react"
|
className="module-message__buttons__react"
|
||||||
|
aria-label={i18n('reactToMessage')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -1178,6 +1199,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
|
|
||||||
const replyButton = (
|
const replyButton = (
|
||||||
|
// This a menu meant for mouse use only
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// eslint-disable-next-line jsx-a11y/interactive-supports-focus, jsx-a11y/click-events-have-key-events
|
||||||
<div
|
<div
|
||||||
onClick={(event: React.MouseEvent) => {
|
onClick={(event: React.MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -1187,6 +1211,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}}
|
}}
|
||||||
// This a menu meant for mouse use only
|
// This a menu meant for mouse use only
|
||||||
role="button"
|
role="button"
|
||||||
|
aria-label={i18n('replyToMessage')}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__buttons__reply',
|
'module-message__buttons__reply',
|
||||||
`module-message__buttons__download--${direction}`
|
`module-message__buttons__download--${direction}`
|
||||||
|
@ -1194,6 +1219,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// This a menu meant for mouse use only
|
||||||
|
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
const menuButton = (
|
const menuButton = (
|
||||||
<Reference>
|
<Reference>
|
||||||
{({ ref: popperRef }) => {
|
{({ ref: popperRef }) => {
|
||||||
|
@ -1205,13 +1233,14 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return (
|
return (
|
||||||
<ContextMenuTrigger
|
<ContextMenuTrigger
|
||||||
id={triggerId}
|
id={triggerId}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
ref={this.captureMenuTrigger as any}
|
ref={this.captureMenuTrigger as any}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
// This a menu meant for mouse use only
|
|
||||||
ref={maybePopperRef}
|
ref={maybePopperRef}
|
||||||
role="button"
|
role="button"
|
||||||
onClick={this.showMenu}
|
onClick={this.showMenu}
|
||||||
|
aria-label={i18n('messageContextMenuButton')}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__buttons__menu',
|
'module-message__buttons__menu',
|
||||||
`module-message__buttons__download--${direction}`
|
`module-message__buttons__download--${direction}`
|
||||||
|
@ -1222,6 +1251,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}}
|
}}
|
||||||
</Reference>
|
</Reference>
|
||||||
);
|
);
|
||||||
|
/* eslint-enable jsx-a11y/interactive-supports-focus */
|
||||||
|
/* eslint-enable jsx-a11y/click-events-have-key-events */
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Manager>
|
<Manager>
|
||||||
|
@ -1238,19 +1269,20 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
{reactionPickerRoot &&
|
{reactionPickerRoot &&
|
||||||
createPortal(
|
createPortal(
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
<Popper placement="top">
|
<Popper placement="top">
|
||||||
{({ ref, style }) => (
|
{({ ref, style }) => (
|
||||||
<ReactionPicker
|
<ReactionPicker
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={style}
|
style={style}
|
||||||
selected={this.props.selectedReaction}
|
selected={selectedReaction}
|
||||||
onClose={this.toggleReactionPicker}
|
onClose={this.toggleReactionPicker}
|
||||||
onPick={emoji => {
|
onPick={emoji => {
|
||||||
this.toggleReactionPicker(true);
|
this.toggleReactionPicker(true);
|
||||||
this.props.reactToMessage(id, {
|
reactToMessage(id, {
|
||||||
emoji,
|
emoji,
|
||||||
remove: emoji === this.props.selectedReaction,
|
remove: emoji === selectedReaction,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
|
@ -1263,8 +1295,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length
|
public renderContextMenu(triggerId: string): JSX.Element {
|
||||||
public renderContextMenu(triggerId: string) {
|
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
canReply,
|
canReply,
|
||||||
|
@ -1396,7 +1427,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
const first = previews[0];
|
const first = previews[0];
|
||||||
|
|
||||||
if (!first || !first.image) {
|
if (!first || !first.image) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
const { width } = first.image;
|
const { width } = first.image;
|
||||||
|
|
||||||
|
@ -1414,9 +1445,11 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Messy return here.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
public isShowingImage() {
|
public isShowingImage() {
|
||||||
const { isTapToView, attachments, previews } = this.props;
|
const { isTapToView, attachments, previews } = this.props;
|
||||||
const { imageBroken } = this.state;
|
const { imageBroken } = this.state;
|
||||||
|
@ -1449,7 +1482,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAttachmentPending() {
|
public isAttachmentPending(): boolean {
|
||||||
const { attachments } = this.props;
|
const { attachments } = this.props;
|
||||||
|
|
||||||
if (!attachments || attachments.length < 1) {
|
if (!attachments || attachments.length < 1) {
|
||||||
|
@ -1461,7 +1494,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return Boolean(first.pending);
|
return Boolean(first.pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderTapToViewIcon() {
|
public renderTapToViewIcon(): JSX.Element {
|
||||||
const { direction, isTapToViewExpired } = this.props;
|
const { direction, isTapToViewExpired } = this.props;
|
||||||
const isDownloadPending = this.isAttachmentPending();
|
const isDownloadPending = this.isAttachmentPending();
|
||||||
|
|
||||||
|
@ -1482,7 +1515,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderTapToViewText() {
|
public renderTapToViewText(): string | undefined {
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
direction,
|
direction,
|
||||||
|
@ -1505,6 +1538,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return, no-nested-ternary
|
||||||
return isTapToViewError
|
return isTapToViewError
|
||||||
? i18n('incomingError')
|
? i18n('incomingError')
|
||||||
: direction === 'outgoing'
|
: direction === 'outgoing'
|
||||||
|
@ -1512,7 +1546,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
: incomingString;
|
: incomingString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderTapToView() {
|
public renderTapToView(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
collapseMetadata,
|
collapseMetadata,
|
||||||
conversationType,
|
conversationType,
|
||||||
|
@ -1558,7 +1592,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleReactionViewer = (onlyRemove = false) => {
|
public toggleReactionViewer = (onlyRemove = false): void => {
|
||||||
this.setState(({ reactionViewerRoot }) => {
|
this.setState(({ reactionViewerRoot }) => {
|
||||||
if (reactionViewerRoot) {
|
if (reactionViewerRoot) {
|
||||||
document.body.removeChild(reactionViewerRoot);
|
document.body.removeChild(reactionViewerRoot);
|
||||||
|
@ -1589,7 +1623,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public toggleReactionPicker = (onlyRemove = false) => {
|
public toggleReactionPicker = (onlyRemove = false): void => {
|
||||||
this.setState(({ reactionPickerRoot }) => {
|
this.setState(({ reactionPickerRoot }) => {
|
||||||
if (reactionPickerRoot) {
|
if (reactionPickerRoot) {
|
||||||
document.body.removeChild(reactionPickerRoot);
|
document.body.removeChild(reactionPickerRoot);
|
||||||
|
@ -1620,7 +1654,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleClickOutsideReactionViewer = (e: MouseEvent) => {
|
public handleClickOutsideReactionViewer = (e: MouseEvent): void => {
|
||||||
const { reactionViewerRoot } = this.state;
|
const { reactionViewerRoot } = this.state;
|
||||||
const { current: reactionsContainer } = this.reactionsContainerRef;
|
const { current: reactionsContainer } = this.reactionsContainerRef;
|
||||||
if (reactionViewerRoot && reactionsContainer) {
|
if (reactionViewerRoot && reactionsContainer) {
|
||||||
|
@ -1633,7 +1667,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleClickOutsideReactionPicker = (e: MouseEvent) => {
|
public handleClickOutsideReactionPicker = (e: MouseEvent): void => {
|
||||||
const { reactionPickerRoot } = this.state;
|
const { reactionPickerRoot } = this.state;
|
||||||
if (reactionPickerRoot) {
|
if (reactionPickerRoot) {
|
||||||
if (!reactionPickerRoot.contains(e.target as HTMLElement)) {
|
if (!reactionPickerRoot.contains(e.target as HTMLElement)) {
|
||||||
|
@ -1642,8 +1676,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length
|
public renderReactions(outgoing: boolean): JSX.Element | null {
|
||||||
public renderReactions(outgoing: boolean) {
|
|
||||||
const { reactions, i18n } = this.props;
|
const { reactions, i18n } = this.props;
|
||||||
|
|
||||||
if (!reactions || (reactions && reactions.length === 0)) {
|
if (!reactions || (reactions && reactions.length === 0)) {
|
||||||
|
@ -1726,6 +1759,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
key={`${re.emoji}-${i}`}
|
key={`${re.emoji}-${i}`}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__reactions__reaction',
|
'module-message__reactions__reaction',
|
||||||
|
@ -1764,7 +1799,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
+{maybeNotRenderedTotal}
|
+{maybeNotRenderedTotal}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>
|
<>
|
||||||
<Emoji size={16} emoji={re.emoji} />
|
<Emoji size={16} emoji={re.emoji} />
|
||||||
{re.count > 1 ? (
|
{re.count > 1 ? (
|
||||||
<span
|
<span
|
||||||
|
@ -1778,7 +1813,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
{re.count}
|
{re.count}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</React.Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1808,7 +1843,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderContents() {
|
public renderContents(): JSX.Element | null {
|
||||||
const { isTapToView, deletedForEveryone } = this.props;
|
const { isTapToView, deletedForEveryone } = this.props;
|
||||||
|
|
||||||
if (deletedForEveryone) {
|
if (deletedForEveryone) {
|
||||||
|
@ -1837,10 +1872,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
|
||||||
public handleOpen = (
|
public handleOpen = (
|
||||||
event: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent
|
event: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent
|
||||||
) => {
|
): void => {
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
contact,
|
contact,
|
||||||
|
@ -1923,10 +1957,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (this.audioRef.current.paused) {
|
if (this.audioRef.current.paused) {
|
||||||
// tslint:disable-next-line no-floating-promises
|
|
||||||
this.audioRef.current.play();
|
this.audioRef.current.play();
|
||||||
} else {
|
} else {
|
||||||
// tslint:disable-next-line no-floating-promises
|
|
||||||
this.audioRef.current.pause();
|
this.audioRef.current.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1946,7 +1978,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public openGenericAttachment = (event?: React.MouseEvent) => {
|
public openGenericAttachment = (event?: React.MouseEvent): void => {
|
||||||
const { attachments, downloadAttachment, timestamp } = this.props;
|
const { attachments, downloadAttachment, timestamp } = this.props;
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
|
@ -1969,7 +2001,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||||
// Do not allow reactions to error messages
|
// Do not allow reactions to error messages
|
||||||
const { canReply } = this.props;
|
const { canReply } = this.props;
|
||||||
|
|
||||||
|
@ -1989,7 +2021,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
this.handleOpen(event);
|
this.handleOpen(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleClick = (event: React.MouseEvent) => {
|
public handleClick = (event: React.MouseEvent): void => {
|
||||||
// We don't want clicks on body text to result in the 'default action' for the message
|
// We don't want clicks on body text to result in the 'default action' for the message
|
||||||
const { text } = this.props;
|
const { text } = this.props;
|
||||||
if (text && text.length > 0) {
|
if (text && text.length > 0) {
|
||||||
|
@ -2008,8 +2040,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
this.handleOpen(event);
|
this.handleOpen(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line: cyclomatic-complexity
|
public renderContainer(): JSX.Element {
|
||||||
public renderContainer() {
|
|
||||||
const {
|
const {
|
||||||
authorColor,
|
authorColor,
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
|
@ -2061,7 +2092,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Measure
|
<Measure
|
||||||
bounds={true}
|
bounds
|
||||||
onResize={({ bounds = { width: 0 } }) => {
|
onResize={({ bounds = { width: 0 } }) => {
|
||||||
this.setState({ containerWidth: bounds.width });
|
this.setState({ containerWidth: bounds.width });
|
||||||
}}
|
}}
|
||||||
|
@ -2081,8 +2112,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity
|
public render(): JSX.Element | null {
|
||||||
public render() {
|
|
||||||
const {
|
const {
|
||||||
authorPhoneNumber,
|
authorPhoneNumber,
|
||||||
attachments,
|
attachments,
|
||||||
|
|
|
@ -4,11 +4,9 @@ import { boolean, text } from '@storybook/addon-knobs';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import { MessageBody, Props } from './MessageBody';
|
import { MessageBody, Props } from './MessageBody';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/MessageBody', module);
|
const story = storiesOf('Components/Conversation/MessageBody', module);
|
||||||
|
|
|
@ -94,7 +94,7 @@ export class MessageBody extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
text,
|
text,
|
||||||
|
|
|
@ -6,11 +6,9 @@ import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import { Props as MessageProps } from './Message';
|
import { Props as MessageProps } from './Message';
|
||||||
import { MessageDetail, Props } from './MessageDetail';
|
import { MessageDetail, Props } from './MessageDetail';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/MessageDetail', module);
|
const story = storiesOf('Components/Conversation/MessageDetail', module);
|
||||||
|
@ -147,6 +145,7 @@ story.add('Not Delivered', () => {
|
||||||
text: 'A message to Max',
|
text: 'A message to Max',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
props.receivedAt = undefined as any;
|
props.receivedAt = undefined as any;
|
||||||
|
|
||||||
return <MessageDetail {...props} />;
|
return <MessageDetail {...props} />;
|
||||||
|
|
|
@ -37,10 +37,14 @@ export interface Props {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _keyForError = (error: Error): string => {
|
||||||
|
return `${error.name}-${error.message}`;
|
||||||
|
};
|
||||||
|
|
||||||
export class MessageDetail extends React.Component<Props> {
|
export class MessageDetail extends React.Component<Props> {
|
||||||
private readonly focusRef = React.createRef<HTMLDivElement>();
|
private readonly focusRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount(): void {
|
||||||
// When this component is created, it's initially not part of the DOM, and then it's
|
// When this component is created, it's initially not part of the DOM, and then it's
|
||||||
// added off-screen and animated in. This ensures that the focus takes.
|
// added off-screen and animated in. This ensures that the focus takes.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -50,7 +54,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderAvatar(contact: Contact) {
|
public renderAvatar(contact: Contact): JSX.Element {
|
||||||
const { i18n } = this.props;
|
const { i18n } = this.props;
|
||||||
const {
|
const {
|
||||||
avatarPath,
|
avatarPath,
|
||||||
|
@ -76,12 +80,13 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderDeleteButton() {
|
public renderDeleteButton(): JSX.Element {
|
||||||
const { i18n, message } = this.props;
|
const { i18n, message } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-message-detail__delete-button-container">
|
<div className="module-message-detail__delete-button-container">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
message.deleteMessage(message.id);
|
message.deleteMessage(message.id);
|
||||||
}}
|
}}
|
||||||
|
@ -93,19 +98,21 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderContact(contact: Contact) {
|
public renderContact(contact: Contact): JSX.Element {
|
||||||
const { i18n } = this.props;
|
const { i18n } = this.props;
|
||||||
const errors = contact.errors || [];
|
const errors = contact.errors || [];
|
||||||
|
|
||||||
const errorComponent = contact.isOutgoingKeyError ? (
|
const errorComponent = contact.isOutgoingKeyError ? (
|
||||||
<div className="module-message-detail__contact__error-buttons">
|
<div className="module-message-detail__contact__error-buttons">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="module-message-detail__contact__show-safety-number"
|
className="module-message-detail__contact__show-safety-number"
|
||||||
onClick={contact.onShowSafetyNumber}
|
onClick={contact.onShowSafetyNumber}
|
||||||
>
|
>
|
||||||
{i18n('showSafetyNumber')}
|
{i18n('showSafetyNumber')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="module-message-detail__contact__send-anyway"
|
className="module-message-detail__contact__send-anyway"
|
||||||
onClick={contact.onSendAnyway}
|
onClick={contact.onSendAnyway}
|
||||||
>
|
>
|
||||||
|
@ -138,8 +145,11 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.map((error, index) => (
|
{errors.map(error => (
|
||||||
<div key={index} className="module-message-detail__contact__error">
|
<div
|
||||||
|
key={_keyForError(error)}
|
||||||
|
className="module-message-detail__contact__error"
|
||||||
|
>
|
||||||
{error.message}
|
{error.message}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -151,7 +161,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderContacts() {
|
public renderContacts(): JSX.Element | null {
|
||||||
const { contacts } = this.props;
|
const { contacts } = this.props;
|
||||||
|
|
||||||
if (!contacts || !contacts.length) {
|
if (!contacts || !contacts.length) {
|
||||||
|
@ -165,18 +175,19 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { errors, message, receivedAt, sentAt, i18n } = this.props;
|
const { errors, message, receivedAt, sentAt, i18n } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
|
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
|
||||||
<div className="module-message-detail__message-container">
|
<div className="module-message-detail__message-container">
|
||||||
<Message i18n={i18n} {...message} />
|
<Message i18n={i18n} {...message} />
|
||||||
</div>
|
</div>
|
||||||
<table className="module-message-detail__info">
|
<table className="module-message-detail__info">
|
||||||
<tbody>
|
<tbody>
|
||||||
{(errors || []).map((error, index) => (
|
{(errors || []).map(error => (
|
||||||
<tr key={index}>
|
<tr key={_keyForError(error)}>
|
||||||
<td className="module-message-detail__label">
|
<td className="module-message-detail__label">
|
||||||
{i18n('error')}
|
{i18n('error')}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -2,14 +2,12 @@ import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { text } from '@storybook/addon-knobs';
|
import { text } from '@storybook/addon-knobs';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MessageRequestActions,
|
MessageRequestActions,
|
||||||
Props as MessageRequestActionsProps,
|
Props as MessageRequestActionsProps,
|
||||||
} from './MessageRequestActions';
|
} from './MessageRequestActions';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
@ -42,7 +40,7 @@ storiesOf('Components/Conversation/MessageRequestActions', module)
|
||||||
.add('Direct (Blocked)', () => {
|
.add('Direct (Blocked)', () => {
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '480px' }}>
|
<div style={{ width: '480px' }}>
|
||||||
<MessageRequestActions {...getBaseProps()} isBlocked={true} />
|
<MessageRequestActions {...getBaseProps()} isBlocked />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -56,7 +54,7 @@ storiesOf('Components/Conversation/MessageRequestActions', module)
|
||||||
.add('Group (Blocked)', () => {
|
.add('Group (Blocked)', () => {
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '480px' }}>
|
<div style={{ width: '480px' }}>
|
||||||
<MessageRequestActions {...getBaseProps(true)} isBlocked={true} />
|
<MessageRequestActions {...getBaseProps(true)} isBlocked />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,7 +19,6 @@ export type Props = {
|
||||||
'i18n' | 'state' | 'onChangeState'
|
'i18n' | 'state' | 'onChangeState'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length
|
|
||||||
export const MessageRequestActions = ({
|
export const MessageRequestActions = ({
|
||||||
conversationType,
|
conversationType,
|
||||||
firstName,
|
firstName,
|
||||||
|
@ -34,7 +33,7 @@ export const MessageRequestActions = ({
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
profileName,
|
profileName,
|
||||||
title,
|
title,
|
||||||
}: Props) => {
|
}: Props): JSX.Element => {
|
||||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -80,6 +79,7 @@ export const MessageRequestActions = ({
|
||||||
</p>
|
</p>
|
||||||
<div className="module-message-request-actions__buttons">
|
<div className="module-message-request-actions__buttons">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMrState(MessageRequestState.deleting);
|
setMrState(MessageRequestState.deleting);
|
||||||
}}
|
}}
|
||||||
|
@ -93,6 +93,7 @@ export const MessageRequestActions = ({
|
||||||
</button>
|
</button>
|
||||||
{isBlocked ? (
|
{isBlocked ? (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMrState(MessageRequestState.unblocking);
|
setMrState(MessageRequestState.unblocking);
|
||||||
}}
|
}}
|
||||||
|
@ -106,6 +107,7 @@ export const MessageRequestActions = ({
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMrState(MessageRequestState.blocking);
|
setMrState(MessageRequestState.blocking);
|
||||||
}}
|
}}
|
||||||
|
@ -120,6 +122,7 @@ export const MessageRequestActions = ({
|
||||||
)}
|
)}
|
||||||
{!isBlocked ? (
|
{!isBlocked ? (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onAccept}
|
onClick={onAccept}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
@ -23,7 +23,6 @@ export type Props = {
|
||||||
onChangeState(state: MessageRequestState): unknown;
|
onChangeState(state: MessageRequestState): unknown;
|
||||||
} & Omit<ContactNameProps, 'module' | 'i18n'>;
|
} & Omit<ContactNameProps, 'module' | 'i18n'>;
|
||||||
|
|
||||||
// tslint:disable-next-line: max-func-body-length
|
|
||||||
export const MessageRequestActionsConfirmation = ({
|
export const MessageRequestActionsConfirmation = ({
|
||||||
conversationType,
|
conversationType,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -37,10 +36,9 @@ export const MessageRequestActionsConfirmation = ({
|
||||||
profileName,
|
profileName,
|
||||||
state,
|
state,
|
||||||
title,
|
title,
|
||||||
}: Props) => {
|
}: Props): JSX.Element | null => {
|
||||||
if (state === MessageRequestState.blocking) {
|
if (state === MessageRequestState.blocking) {
|
||||||
return (
|
return (
|
||||||
// tslint:disable-next-line: use-simple-attributes
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
@ -82,7 +80,6 @@ export const MessageRequestActionsConfirmation = ({
|
||||||
|
|
||||||
if (state === MessageRequestState.unblocking) {
|
if (state === MessageRequestState.unblocking) {
|
||||||
return (
|
return (
|
||||||
// tslint:disable-next-line: use-simple-attributes
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
@ -91,7 +88,7 @@ export const MessageRequestActionsConfirmation = ({
|
||||||
title={
|
title={
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={'MessageRequests--unblock-confirm-title'}
|
id="MessageRequests--unblock-confirm-title"
|
||||||
components={[
|
components={[
|
||||||
<ContactName
|
<ContactName
|
||||||
key="name"
|
key="name"
|
||||||
|
@ -119,7 +116,6 @@ export const MessageRequestActionsConfirmation = ({
|
||||||
|
|
||||||
if (state === MessageRequestState.deleting) {
|
if (state === MessageRequestState.deleting) {
|
||||||
return (
|
return (
|
||||||
// tslint:disable-next-line: use-simple-attributes
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|
|
@ -2,11 +2,8 @@ import * as React from 'react';
|
||||||
|
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import enMessages from '../../../\_locales/en/messages.json';
|
|
||||||
|
|
||||||
import { ProfileChangeNotification } from './ProfileChangeNotification';
|
import { ProfileChangeNotification } from './ProfileChangeNotification';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
|
@ -9,11 +9,9 @@ import { pngUrl } from '../../storybook/Fixtures';
|
||||||
import { Message, Props as MessagesProps } from './Message';
|
import { Message, Props as MessagesProps } from './Message';
|
||||||
import { AUDIO_MP3, IMAGE_PNG, MIMEType, VIDEO_MP4 } from '../../types/MIME';
|
import { AUDIO_MP3, IMAGE_PNG, MIMEType, VIDEO_MP4 } from '../../types/MIME';
|
||||||
import { Props, Quote } from './Quote';
|
import { Props, Quote } from './Quote';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/Quote', module);
|
const story = storiesOf('Components/Conversation/Quote', module);
|
||||||
|
@ -63,7 +61,7 @@ const renderInMessage = ({
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const messageProps = {
|
const messageProps = {
|
||||||
...defaultMessageProps,
|
...defaultMessageProps,
|
||||||
authorColor: authorColor,
|
authorColor,
|
||||||
quote: {
|
quote: {
|
||||||
attachment,
|
attachment,
|
||||||
authorId: 'an-author',
|
authorId: 'an-author',
|
||||||
|
@ -186,6 +184,7 @@ story.add('Image Only', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
props.text = undefined as any;
|
props.text = undefined as any;
|
||||||
|
|
||||||
return <Quote {...props} />;
|
return <Quote {...props} />;
|
||||||
|
@ -230,6 +229,7 @@ story.add('Video Only', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
props.text = undefined as any;
|
props.text = undefined as any;
|
||||||
|
|
||||||
return <Quote {...props} />;
|
return <Quote {...props} />;
|
||||||
|
@ -271,6 +271,7 @@ story.add('Audio Only', () => {
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
props.text = undefined as any;
|
props.text = undefined as any;
|
||||||
|
|
||||||
return <Quote {...props} />;
|
return <Quote {...props} />;
|
||||||
|
@ -296,6 +297,7 @@ story.add('Voice Message Only', () => {
|
||||||
isVoiceMessage: true,
|
isVoiceMessage: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
props.text = undefined as any;
|
props.text = undefined as any;
|
||||||
|
|
||||||
return <Quote {...props} />;
|
return <Quote {...props} />;
|
||||||
|
@ -321,6 +323,7 @@ story.add('Other File Only', () => {
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
props.text = undefined as any;
|
props.text = undefined as any;
|
||||||
|
|
||||||
return <Quote {...props} />;
|
return <Quote {...props} />;
|
||||||
|
@ -355,6 +358,7 @@ story.add('Message Not Found', () => {
|
||||||
|
|
||||||
story.add('Missing Text & Attachment', () => {
|
story.add('Missing Text & Attachment', () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
props.text = undefined as any;
|
props.text = undefined as any;
|
||||||
|
|
||||||
return <Quote {...props} />;
|
return <Quote {...props} />;
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
// tslint:disable:react-this-binding-issue
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import * as MIME from '../../../ts/types/MIME';
|
import * as MIME from '../../types/MIME';
|
||||||
import * as GoogleChrome from '../../../ts/util/GoogleChrome';
|
import * as GoogleChrome from '../../util/GoogleChrome';
|
||||||
|
|
||||||
import { MessageBody } from './MessageBody';
|
import { MessageBody } from './MessageBody';
|
||||||
import { BodyRangesType, LocalizerType } from '../../types/Util';
|
import { BodyRangesType, LocalizerType } from '../../types/Util';
|
||||||
|
@ -65,7 +63,7 @@ function getObjectUrl(thumbnail: Attachment | undefined): string | undefined {
|
||||||
return thumbnail.objectUrl;
|
return thumbnail.objectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeLabel({
|
function getTypeLabel({
|
||||||
|
@ -86,19 +84,21 @@ function getTypeLabel({
|
||||||
if (MIME.isAudio(contentType) && isVoiceMessage) {
|
if (MIME.isAudio(contentType) && isVoiceMessage) {
|
||||||
return i18n('voiceMessage');
|
return i18n('voiceMessage');
|
||||||
}
|
}
|
||||||
if (MIME.isAudio(contentType)) {
|
|
||||||
return i18n('audio');
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return MIME.isAudio(contentType) ? i18n('audio') : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Quote extends React.Component<Props, State> {
|
export class Quote extends React.Component<Props, State> {
|
||||||
public state = {
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
imageBroken: false,
|
imageBroken: false,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
public handleKeyDown = (
|
||||||
|
event: React.KeyboardEvent<HTMLButtonElement>
|
||||||
|
): void => {
|
||||||
const { onClick } = this.props;
|
const { onClick } = this.props;
|
||||||
|
|
||||||
// This is important to ensure that using this quote to navigate to the referenced
|
// This is important to ensure that using this quote to navigate to the referenced
|
||||||
|
@ -109,7 +109,8 @@ export class Quote extends React.Component<Props, State> {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
public handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
|
public handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||||
const { onClick } = this.props;
|
const { onClick } = this.props;
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
|
@ -119,15 +120,20 @@ export class Quote extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleImageError = () => {
|
public handleImageError = (): void => {
|
||||||
// tslint:disable-next-line no-console
|
window.console.info(
|
||||||
console.log('Message: Image failed to load; failing over to placeholder');
|
'Message: Image failed to load; failing over to placeholder'
|
||||||
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
imageBroken: true,
|
imageBroken: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public renderImage(url: string, i18n: LocalizerType, icon?: string) {
|
public renderImage(
|
||||||
|
url: string,
|
||||||
|
i18n: LocalizerType,
|
||||||
|
icon?: string
|
||||||
|
): JSX.Element {
|
||||||
const iconElement = icon ? (
|
const iconElement = icon ? (
|
||||||
<div className="module-quote__icon-container__inner">
|
<div className="module-quote__icon-container__inner">
|
||||||
<div className="module-quote__icon-container__circle-background">
|
<div className="module-quote__icon-container__circle-background">
|
||||||
|
@ -153,7 +159,8 @@ export class Quote extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderIcon(icon: string) {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
public renderIcon(icon: string): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="module-quote__icon-container">
|
<div className="module-quote__icon-container">
|
||||||
<div className="module-quote__icon-container__inner">
|
<div className="module-quote__icon-container__inner">
|
||||||
|
@ -170,11 +177,11 @@ export class Quote extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderGenericFile() {
|
public renderGenericFile(): JSX.Element | null {
|
||||||
const { attachment, isIncoming } = this.props;
|
const { attachment, isIncoming } = this.props;
|
||||||
|
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fileName, contentType } = attachment;
|
const { fileName, contentType } = attachment;
|
||||||
|
@ -202,7 +209,7 @@ export class Quote extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderIconContainer() {
|
public renderIconContainer(): JSX.Element | null {
|
||||||
const { attachment, i18n } = this.props;
|
const { attachment, i18n } = this.props;
|
||||||
const { imageBroken } = this.state;
|
const { imageBroken } = this.state;
|
||||||
|
|
||||||
|
@ -283,8 +290,8 @@ export class Quote extends React.Component<Props, State> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderClose() {
|
public renderClose(): JSX.Element | null {
|
||||||
const { onClose } = this.props;
|
const { i18n, onClose } = this.props;
|
||||||
|
|
||||||
if (!onClose) {
|
if (!onClose) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -313,6 +320,7 @@ export class Quote extends React.Component<Props, State> {
|
||||||
// We can't be a button because the overall quote is a button; can't nest them
|
// We can't be a button because the overall quote is a button; can't nest them
|
||||||
role="button"
|
role="button"
|
||||||
className="module-quote__close-button"
|
className="module-quote__close-button"
|
||||||
|
aria-label={i18n('close')}
|
||||||
onKeyDown={keyDownHandler}
|
onKeyDown={keyDownHandler}
|
||||||
onClick={clickHandler}
|
onClick={clickHandler}
|
||||||
/>
|
/>
|
||||||
|
@ -320,7 +328,7 @@ export class Quote extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderAuthor() {
|
public renderAuthor(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
authorProfileName,
|
authorProfileName,
|
||||||
authorPhoneNumber,
|
authorPhoneNumber,
|
||||||
|
@ -353,7 +361,7 @@ export class Quote extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderReferenceWarning() {
|
public renderReferenceWarning(): JSX.Element | null {
|
||||||
const { i18n, isIncoming, referencedMessageNotFound } = this.props;
|
const { i18n, isIncoming, referencedMessageNotFound } = this.props;
|
||||||
|
|
||||||
if (!referencedMessageNotFound) {
|
if (!referencedMessageNotFound) {
|
||||||
|
@ -389,7 +397,7 @@ export class Quote extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
authorColor,
|
authorColor,
|
||||||
isIncoming,
|
isIncoming,
|
||||||
|
@ -410,6 +418,7 @@ export class Quote extends React.Component<Props, State> {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
@ -65,6 +65,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
key={emoji}
|
key={emoji}
|
||||||
ref={maybeFocusRef}
|
ref={maybeFocusRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
@ -87,6 +88,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-reaction-picker__emoji-btn',
|
'module-reaction-picker__emoji-btn',
|
||||||
otherSelected
|
otherSelected
|
||||||
|
|
|
@ -4,11 +4,9 @@ import { action } from '@storybook/addon-actions';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import { Props, ReactionViewer } from './ReactionViewer';
|
import { Props, ReactionViewer } from './ReactionViewer';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/ReactionViewer', module);
|
const story = storiesOf('Components/Conversation/ReactionViewer', module);
|
||||||
|
|
|
@ -35,7 +35,6 @@ export type Props = OwnProps &
|
||||||
const emojisOrder = ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'];
|
const emojisOrder = ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'];
|
||||||
|
|
||||||
export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
|
export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
|
||||||
// tslint:disable-next-line max-func-body-length
|
|
||||||
({ i18n, reactions, onClose, pickedReaction, ...rest }, ref) => {
|
({ i18n, reactions, onClose, pickedReaction, ...rest }, ref) => {
|
||||||
const grouped = mapValues(groupBy(reactions, 'emoji'), res =>
|
const grouped = mapValues(groupBy(reactions, 'emoji'), res =>
|
||||||
orderBy(res, ['timestamp'], ['desc'])
|
orderBy(res, ['timestamp'], ['desc'])
|
||||||
|
@ -112,6 +111,7 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
key={cat}
|
key={cat}
|
||||||
ref={maybeFocusRef}
|
ref={maybeFocusRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
@ -3,11 +3,9 @@ import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import { ResetSessionNotification } from './ResetSessionNotification';
|
import { ResetSessionNotification } from './ResetSessionNotification';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf(
|
const story = storiesOf(
|
||||||
|
|
|
@ -6,14 +6,8 @@ export interface Props {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResetSessionNotification extends React.Component<Props> {
|
export const ResetSessionNotification = ({ i18n }: Props): JSX.Element => (
|
||||||
public render() {
|
|
||||||
const { i18n } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="module-reset-session-notification">
|
<div className="module-reset-session-notification">
|
||||||
{i18n('sessionEnded')}
|
{i18n('sessionEnded')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,12 +3,8 @@ import { storiesOf } from '@storybook/react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { boolean, text } from '@storybook/addon-knobs';
|
import { boolean, text } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ContactType,
|
ContactType,
|
||||||
Props,
|
Props,
|
||||||
|
|
|
@ -27,9 +27,12 @@ export type PropsActions = {
|
||||||
|
|
||||||
export type Props = PropsData & PropsHousekeeping & PropsActions;
|
export type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||||
|
|
||||||
export class SafetyNumberNotification extends React.Component<Props> {
|
export const SafetyNumberNotification = ({
|
||||||
public render() {
|
contact,
|
||||||
const { contact, isGroup, i18n, showIdentity } = this.props;
|
isGroup,
|
||||||
|
i18n,
|
||||||
|
showIdentity,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
const changeKey = isGroup
|
const changeKey = isGroup
|
||||||
? 'safetyNumberChangedGroup'
|
? 'safetyNumberChangedGroup'
|
||||||
: 'safetyNumberChanged';
|
: 'safetyNumberChanged';
|
||||||
|
@ -59,6 +62,7 @@ export class SafetyNumberNotification extends React.Component<Props> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showIdentity(contact.id);
|
showIdentity(contact.id);
|
||||||
}}
|
}}
|
||||||
|
@ -68,5 +72,4 @@ export class SafetyNumberNotification extends React.Component<Props> {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -3,12 +3,8 @@ import { storiesOf } from '@storybook/react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { boolean } from '@storybook/addon-knobs';
|
import { boolean } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Props, ScrollDownButton } from './ScrollDownButton';
|
import { Props, ScrollDownButton } from './ScrollDownButton';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
|
@ -12,16 +12,18 @@ export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ScrollDownButton extends React.Component<Props> {
|
export const ScrollDownButton = ({
|
||||||
public render() {
|
conversationId,
|
||||||
const { conversationId, withNewMessages, i18n, scrollDown } = this.props;
|
withNewMessages,
|
||||||
const altText = withNewMessages
|
i18n,
|
||||||
? i18n('messagesBelow')
|
scrollDown,
|
||||||
: i18n('scrollDown');
|
}: Props): JSX.Element => {
|
||||||
|
const altText = withNewMessages ? i18n('messagesBelow') : i18n('scrollDown');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-scroll-down">
|
<div className="module-scroll-down">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-scroll-down__button',
|
'module-scroll-down__button',
|
||||||
withNewMessages ? 'module-scroll-down__button--new-messages' : null
|
withNewMessages ? 'module-scroll-down__button--new-messages' : null
|
||||||
|
@ -35,5 +37,4 @@ export class ScrollDownButton extends React.Component<Props> {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -5,13 +5,8 @@ import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
import { AttachmentType } from '../../types/Attachment';
|
import { AttachmentType } from '../../types/Attachment';
|
||||||
import { MIMEType } from '../../types/MIME';
|
import { MIMEType } from '../../types/MIME';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Props, StagedGenericAttachment } from './StagedGenericAttachment';
|
import { Props, StagedGenericAttachment } from './StagedGenericAttachment';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
|
@ -9,16 +9,20 @@ export interface Props {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StagedGenericAttachment extends React.Component<Props> {
|
export const StagedGenericAttachment = ({
|
||||||
public render() {
|
attachment,
|
||||||
const { attachment, onClose } = this.props;
|
i18n,
|
||||||
|
onClose,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
const { fileName, contentType } = attachment;
|
const { fileName, contentType } = attachment;
|
||||||
const extension = getExtensionForDisplay({ contentType, fileName });
|
const extension = getExtensionForDisplay({ contentType, fileName });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-staged-generic-attachment">
|
<div className="module-staged-generic-attachment">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="module-staged-generic-attachment__close-button"
|
className="module-staged-generic-attachment__close-button"
|
||||||
|
aria-label={i18n('close')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose(attachment);
|
onClose(attachment);
|
||||||
|
@ -37,5 +41,4 @@ export class StagedGenericAttachment extends React.Component<Props> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -5,19 +5,15 @@ import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
import { AttachmentType } from '../../types/Attachment';
|
import { AttachmentType } from '../../types/Attachment';
|
||||||
import { MIMEType } from '../../types/MIME';
|
import { MIMEType } from '../../types/MIME';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Props, StagedLinkPreview } from './StagedLinkPreview';
|
import { Props, StagedLinkPreview } from './StagedLinkPreview';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/StagedLinkPreview', module);
|
const story = storiesOf('Components/Conversation/StagedLinkPreview', module);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
||||||
|
|
||||||
const createAttachment = (
|
const createAttachment = (
|
||||||
|
|
|
@ -16,10 +16,14 @@ export interface Props {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StagedLinkPreview extends React.Component<Props> {
|
export const StagedLinkPreview = ({
|
||||||
public render() {
|
isLoaded,
|
||||||
const { isLoaded, onClose, i18n, title, image, domain } = this.props;
|
onClose,
|
||||||
|
i18n,
|
||||||
|
title,
|
||||||
|
image,
|
||||||
|
domain,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
const isImage = image && isImageAttachment(image);
|
const isImage = image && isImageAttachment(image);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -38,7 +42,7 @@ export class StagedLinkPreview extends React.Component<Props> {
|
||||||
<div className="module-staged-link-preview__icon-container">
|
<div className="module-staged-link-preview__icon-container">
|
||||||
<Image
|
<Image
|
||||||
alt={i18n('stagedPreviewThumbnail', [domain])}
|
alt={i18n('stagedPreviewThumbnail', [domain])}
|
||||||
softCorners={true}
|
softCorners
|
||||||
height={72}
|
height={72}
|
||||||
width={72}
|
width={72}
|
||||||
url={image.url}
|
url={image.url}
|
||||||
|
@ -54,10 +58,11 @@ export class StagedLinkPreview extends React.Component<Props> {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="module-staged-link-preview__close-button"
|
className="module-staged-link-preview__close-button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
aria-label={i18n('close')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -2,12 +2,8 @@ import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment';
|
import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
|
@ -6,12 +6,12 @@ interface Props {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StagedPlaceholderAttachment extends React.Component<Props> {
|
export const StagedPlaceholderAttachment = ({
|
||||||
public render() {
|
i18n,
|
||||||
const { i18n, onClick } = this.props;
|
onClick,
|
||||||
|
}: Props): JSX.Element => (
|
||||||
return (
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="module-staged-placeholder-attachment"
|
className="module-staged-placeholder-attachment"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
title={i18n('add-image-attachment')}
|
title={i18n('add-image-attachment')}
|
||||||
|
@ -19,5 +19,3 @@ export class StagedPlaceholderAttachment extends React.Component<Props> {
|
||||||
<div className="module-staged-placeholder-attachment__plus-icon" />
|
<div className="module-staged-placeholder-attachment__plus-icon" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,12 +3,8 @@ import { storiesOf } from '@storybook/react';
|
||||||
import { boolean, number } from '@storybook/addon-knobs';
|
import { boolean, number } from '@storybook/addon-knobs';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Props, Timeline } from './Timeline';
|
import { Props, Timeline } from './Timeline';
|
||||||
import { TimelineItem, TimelineItemType } from './TimelineItem';
|
import { TimelineItem, TimelineItemType } from './TimelineItem';
|
||||||
import { LastSeenIndicator } from './LastSeenIndicator';
|
import { LastSeenIndicator } from './LastSeenIndicator';
|
||||||
|
@ -19,7 +15,7 @@ const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/Timeline', module);
|
const story = storiesOf('Components/Conversation/Timeline', module);
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// eslint-disable-next-line
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
Object.assign(window, {
|
Object.assign(window, {
|
||||||
|
@ -207,6 +203,7 @@ const items: Record<string, TimelineItemType> = {
|
||||||
type: 'linkNotification',
|
type: 'linkNotification',
|
||||||
data: null,
|
data: null,
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const actions = () => ({
|
const actions = () => ({
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { debounce, get, isNumber } from 'lodash';
|
import { debounce, get, isNumber } from 'lodash';
|
||||||
import React from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import {
|
import {
|
||||||
AutoSizer,
|
AutoSizer,
|
||||||
CellMeasurer,
|
CellMeasurer,
|
||||||
CellMeasurerCache,
|
CellMeasurerCache,
|
||||||
List,
|
List,
|
||||||
|
Grid,
|
||||||
} from 'react-virtualized';
|
} from 'react-virtualized';
|
||||||
|
|
||||||
import { ScrollDownButton } from './ScrollDownButton';
|
import { ScrollDownButton } from './ScrollDownButton';
|
||||||
|
@ -39,7 +40,7 @@ export type PropsDataType = {
|
||||||
type PropsHousekeepingType = {
|
type PropsHousekeepingType = {
|
||||||
id: string;
|
id: string;
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
typingContact?: Object;
|
typingContact?: unknown;
|
||||||
selectedMessageId?: string;
|
selectedMessageId?: string;
|
||||||
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -47,7 +48,7 @@ type PropsHousekeepingType = {
|
||||||
renderItem: (
|
renderItem: (
|
||||||
id: string,
|
id: string,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
actions: Object
|
actions: Record<string, unknown>
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
renderLastSeenIndicator: (id: string) => JSX.Element;
|
renderLastSeenIndicator: (id: string) => JSX.Element;
|
||||||
renderHeroRow: (
|
renderHeroRow: (
|
||||||
|
@ -86,8 +87,8 @@ type RowRendererParamsType = {
|
||||||
isScrolling: boolean;
|
isScrolling: boolean;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
key: string;
|
key: string;
|
||||||
parent: Object;
|
parent: Record<string, unknown>;
|
||||||
style: Object;
|
style: CSSProperties;
|
||||||
};
|
};
|
||||||
type OnScrollParamsType = {
|
type OnScrollParamsType = {
|
||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
|
@ -134,13 +135,20 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
defaultHeight: 64,
|
defaultHeight: 64,
|
||||||
fixedWidth: true,
|
fixedWidth: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
public mostRecentWidth = 0;
|
public mostRecentWidth = 0;
|
||||||
|
|
||||||
public mostRecentHeight = 0;
|
public mostRecentHeight = 0;
|
||||||
|
|
||||||
public offsetFromBottom: number | undefined = 0;
|
public offsetFromBottom: number | undefined = 0;
|
||||||
|
|
||||||
public resizeFlag = false;
|
public resizeFlag = false;
|
||||||
public listRef = React.createRef<any>();
|
|
||||||
|
public listRef = React.createRef<List>();
|
||||||
|
|
||||||
public visibleRows: VisibleRowsType | undefined;
|
public visibleRows: VisibleRowsType | undefined;
|
||||||
public loadCountdownTimeout: any;
|
|
||||||
|
public loadCountdownTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -176,9 +184,9 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getList = () => {
|
public getList = (): List | null => {
|
||||||
if (!this.listRef) {
|
if (!this.listRef) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { current } = this.listRef;
|
const { current } = this.listRef;
|
||||||
|
@ -186,25 +194,30 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
return current;
|
return current;
|
||||||
};
|
};
|
||||||
|
|
||||||
public getGrid = () => {
|
public getGrid = (): Grid | undefined => {
|
||||||
const list = this.getList();
|
const list = this.getList();
|
||||||
if (!list) {
|
if (!list) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return list.Grid;
|
return list.Grid;
|
||||||
};
|
};
|
||||||
|
|
||||||
public getScrollContainer = () => {
|
public getScrollContainer = (): HTMLDivElement | undefined => {
|
||||||
const grid = this.getGrid();
|
// We're using an internal variable (_scrollingContainer)) here,
|
||||||
|
// so cannot rely on the public type.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const grid: any = this.getGrid();
|
||||||
if (!grid) {
|
if (!grid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return grid._scrollingContainer as HTMLDivElement;
|
return grid._scrollingContainer as HTMLDivElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
public scrollToRow = (row: number) => {
|
public scrollToRow = (row: number): void => {
|
||||||
const list = this.getList();
|
const list = this.getList();
|
||||||
if (!list) {
|
if (!list) {
|
||||||
return;
|
return;
|
||||||
|
@ -213,7 +226,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
list.scrollToRow(row);
|
list.scrollToRow(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
public recomputeRowHeights = (row?: number) => {
|
public recomputeRowHeights = (row?: number): void => {
|
||||||
const list = this.getList();
|
const list = this.getList();
|
||||||
if (!list) {
|
if (!list) {
|
||||||
return;
|
return;
|
||||||
|
@ -222,7 +235,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
list.recomputeRowHeights(row);
|
list.recomputeRowHeights(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
public onHeightOnlyChange = () => {
|
public onHeightOnlyChange = (): void => {
|
||||||
const grid = this.getGrid();
|
const grid = this.getGrid();
|
||||||
const scrollContainer = this.getScrollContainer();
|
const scrollContainer = this.getScrollContainer();
|
||||||
if (!grid || !scrollContainer) {
|
if (!grid || !scrollContainer) {
|
||||||
|
@ -240,13 +253,18 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
const delta = newOffsetFromBottom - this.offsetFromBottom;
|
const delta = newOffsetFromBottom - this.offsetFromBottom;
|
||||||
|
|
||||||
grid.scrollToPosition({ scrollTop: scrollContainer.scrollTop + delta });
|
// TODO: DESKTOP-687
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(grid as any).scrollToPosition({
|
||||||
|
scrollTop: scrollContainer.scrollTop + delta,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public resize = (row?: number) => {
|
public resize = (row?: number): void => {
|
||||||
this.offsetFromBottom = undefined;
|
this.offsetFromBottom = undefined;
|
||||||
this.resizeFlag = false;
|
this.resizeFlag = false;
|
||||||
if (isNumber(row) && row > 0) {
|
if (isNumber(row) && row > 0) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.cellSizeCache.clearPlus(row, 0);
|
this.cellSizeCache.clearPlus(row, 0);
|
||||||
} else {
|
} else {
|
||||||
|
@ -256,11 +274,11 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
this.recomputeRowHeights(row || 0);
|
this.recomputeRowHeights(row || 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
public resizeHeroRow = () => {
|
public resizeHeroRow = (): void => {
|
||||||
this.resize(0);
|
this.resize(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
public onScroll = (data: OnScrollParamsType) => {
|
public onScroll = (data: OnScrollParamsType): void => {
|
||||||
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
||||||
// re-measures to get us where we want to go.
|
// re-measures to get us where we want to go.
|
||||||
if (
|
if (
|
||||||
|
@ -284,7 +302,6 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
this.updateWithVisibleRows();
|
this.updateWithVisibleRows();
|
||||||
};
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line member-ordering
|
|
||||||
public updateScrollMetrics = debounce(
|
public updateScrollMetrics = debounce(
|
||||||
(data: OnScrollParamsType) => {
|
(data: OnScrollParamsType) => {
|
||||||
const { clientHeight, clientWidth, scrollHeight, scrollTop } = data;
|
const { clientHeight, clientWidth, scrollHeight, scrollTop } = data;
|
||||||
|
@ -337,10 +354,14 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Variable collision
|
||||||
|
// eslint-disable-next-line react/destructuring-assignment
|
||||||
if (loadCountdownStart !== this.props.loadCountdownStart) {
|
if (loadCountdownStart !== this.props.loadCountdownStart) {
|
||||||
setLoadCountdownStart(id, loadCountdownStart);
|
setLoadCountdownStart(id, loadCountdownStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Variable collision
|
||||||
|
// eslint-disable-next-line react/destructuring-assignment
|
||||||
if (isNearBottom !== this.props.isNearBottom) {
|
if (isNearBottom !== this.props.isNearBottom) {
|
||||||
setIsNearBottom(id, isNearBottom);
|
setIsNearBottom(id, isNearBottom);
|
||||||
}
|
}
|
||||||
|
@ -356,7 +377,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
{ maxWait: 50 }
|
{ maxWait: 50 }
|
||||||
);
|
);
|
||||||
|
|
||||||
public updateVisibleRows = () => {
|
public updateVisibleRows = (): void => {
|
||||||
let newest;
|
let newest;
|
||||||
let oldest;
|
let oldest;
|
||||||
|
|
||||||
|
@ -384,6 +405,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
const { id, offsetTop, offsetHeight } = child;
|
const { id, offsetTop, offsetHeight } = child;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,6 +425,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
const { offsetTop, id } = child;
|
const { offsetTop, id } = child;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,7 +440,6 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
this.visibleRows = { newest, oldest };
|
this.visibleRows = { newest, oldest };
|
||||||
};
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line member-ordering cyclomatic-complexity
|
|
||||||
public updateWithVisibleRows = debounce(
|
public updateWithVisibleRows = debounce(
|
||||||
() => {
|
() => {
|
||||||
const {
|
const {
|
||||||
|
@ -479,7 +501,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
{ maxWait: 500 }
|
{ maxWait: 500 }
|
||||||
);
|
);
|
||||||
|
|
||||||
public loadOlderMessages = () => {
|
public loadOlderMessages = (): void => {
|
||||||
const {
|
const {
|
||||||
haveOldest,
|
haveOldest,
|
||||||
isLoadingMessages,
|
isLoadingMessages,
|
||||||
|
@ -505,7 +527,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
key,
|
key,
|
||||||
parent,
|
parent,
|
||||||
style,
|
style,
|
||||||
}: RowRendererParamsType) => {
|
}: RowRendererParamsType): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
haveOldest,
|
haveOldest,
|
||||||
|
@ -591,7 +613,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public fromItemIndexToRow(index: number) {
|
public fromItemIndexToRow(index: number): number {
|
||||||
const { oldestUnreadIndex } = this.props;
|
const { oldestUnreadIndex } = this.props;
|
||||||
|
|
||||||
// We will always render either the hero row or the loading row
|
// We will always render either the hero row or the loading row
|
||||||
|
@ -604,7 +626,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
return index + addition;
|
return index + addition;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRowCount() {
|
public getRowCount(): number {
|
||||||
const { oldestUnreadIndex, typingContact } = this.props;
|
const { oldestUnreadIndex, typingContact } = this.props;
|
||||||
const { items } = this.props;
|
const { items } = this.props;
|
||||||
const itemsCount = items && items.length ? items.length : 0;
|
const itemsCount = items && items.length ? items.length : 0;
|
||||||
|
@ -639,19 +661,21 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLastSeenIndicatorRow(props?: Props) {
|
public getLastSeenIndicatorRow(props?: Props): number | undefined {
|
||||||
const { oldestUnreadIndex } = props || this.props;
|
const { oldestUnreadIndex } = props || this.props;
|
||||||
if (!isNumber(oldestUnreadIndex)) {
|
if (!isNumber(oldestUnreadIndex)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return this.fromItemIndexToRow(oldestUnreadIndex) - 1;
|
return this.fromItemIndexToRow(oldestUnreadIndex) - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTypingBubbleRow() {
|
public getTypingBubbleRow(): number | undefined {
|
||||||
const { items } = this.props;
|
const { items } = this.props;
|
||||||
if (!items || items.length < 0) {
|
if (!items || items.length < 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -659,10 +683,11 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
const last = items.length - 1;
|
const last = items.length - 1;
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return this.fromItemIndexToRow(last) + 1;
|
return this.fromItemIndexToRow(last) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onScrollToMessage = (messageId: string) => {
|
public onScrollToMessage = (messageId: string): void => {
|
||||||
const { isLoadingMessages, items, loadAndScroll } = this.props;
|
const { isLoadingMessages, items, loadAndScroll } = this.props;
|
||||||
const index = items.findIndex(item => item === messageId);
|
const index = items.findIndex(item => item === messageId);
|
||||||
|
|
||||||
|
@ -678,7 +703,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public scrollToBottom = (setFocus?: boolean) => {
|
public scrollToBottom = (setFocus?: boolean): void => {
|
||||||
const { selectMessage, id, items } = this.props;
|
const { selectMessage, id, items } = this.props;
|
||||||
|
|
||||||
if (setFocus && items && items.length > 0) {
|
if (setFocus && items && items.length > 0) {
|
||||||
|
@ -694,11 +719,11 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public onClickScrollDownButton = () => {
|
public onClickScrollDownButton = (): void => {
|
||||||
this.scrollDown(false);
|
this.scrollDown(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
public scrollDown = (setFocus?: boolean) => {
|
public scrollDown = (setFocus?: boolean): void => {
|
||||||
const {
|
const {
|
||||||
haveNewest,
|
haveNewest,
|
||||||
id,
|
id,
|
||||||
|
@ -746,19 +771,20 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.updateWithVisibleRows();
|
this.updateWithVisibleRows();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.registerForActive(this.updateWithVisibleRows);
|
window.registerForActive(this.updateWithVisibleRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.unregisterForActive(this.updateWithVisibleRows);
|
window.unregisterForActive(this.updateWithVisibleRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
public componentDidUpdate(prevProps: Props): void {
|
||||||
public componentDidUpdate(prevProps: Props) {
|
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
clearChangedMessages,
|
clearChangedMessages,
|
||||||
|
@ -787,6 +813,8 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneTimeScrollRow = this.getLastSeenIndicatorRow();
|
const oneTimeScrollRow = this.getLastSeenIndicatorRow();
|
||||||
|
// TODO: DESKTOP-688
|
||||||
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
this.setState({
|
this.setState({
|
||||||
oneTimeScrollRow,
|
oneTimeScrollRow,
|
||||||
atBottom: true,
|
atBottom: true,
|
||||||
|
@ -804,7 +832,9 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
prevProps.items.length > 0 &&
|
prevProps.items.length > 0 &&
|
||||||
items !== prevProps.items
|
items !== prevProps.items
|
||||||
) {
|
) {
|
||||||
if (this.state.atTop) {
|
const { atTop } = this.state;
|
||||||
|
|
||||||
|
if (atTop) {
|
||||||
const oldFirstIndex = 0;
|
const oldFirstIndex = 0;
|
||||||
const oldFirstId = prevProps.items[oldFirstIndex];
|
const oldFirstId = prevProps.items[oldFirstIndex];
|
||||||
|
|
||||||
|
@ -820,6 +850,8 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
// We're loading more new messages at the top; we want to stay at the top
|
// We're loading more new messages at the top; we want to stay at the top
|
||||||
this.resize();
|
this.resize();
|
||||||
|
// TODO: DESKTOP-688
|
||||||
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
this.setState({ oneTimeScrollRow: newRow });
|
this.setState({ oneTimeScrollRow: newRow });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -900,7 +932,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
this.updateWithVisibleRows();
|
this.updateWithVisibleRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScrollTarget = () => {
|
public getScrollTarget = (): number | undefined => {
|
||||||
const { oneTimeScrollRow, atBottom, propScrollToIndex } = this.state;
|
const { oneTimeScrollRow, atBottom, propScrollToIndex } = this.state;
|
||||||
|
|
||||||
const rowCount = this.getRowCount();
|
const rowCount = this.getRowCount();
|
||||||
|
@ -920,7 +952,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
return scrollToBottom;
|
return scrollToBottom;
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleBlur = (event: React.FocusEvent) => {
|
public handleBlur = (event: React.FocusEvent): void => {
|
||||||
const { clearSelectedMessage } = this.props;
|
const { clearSelectedMessage } = this.props;
|
||||||
|
|
||||||
const { currentTarget } = event;
|
const { currentTarget } = event;
|
||||||
|
@ -944,7 +976,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||||
const { selectMessage, selectedMessageId, items, id } = this.props;
|
const { selectMessage, selectedMessageId, items, id } = this.props;
|
||||||
const commandKey = get(window, 'platform') === 'darwin' && event.metaKey;
|
const commandKey = get(window, 'platform') === 'darwin' && event.metaKey;
|
||||||
const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey;
|
const controlKey = get(window, 'platform') !== 'darwin' && event.ctrlKey;
|
||||||
|
@ -1015,12 +1047,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element | null {
|
||||||
const { i18n, id, items } = this.props;
|
const { i18n, id, items } = this.props;
|
||||||
const {
|
const {
|
||||||
shouldShowScrollDownButton,
|
shouldShowScrollDownButton,
|
||||||
|
@ -1037,7 +1067,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="module-timeline"
|
className="module-timeline"
|
||||||
role="group"
|
role="presentation"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
@ -1062,6 +1092,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
<List
|
<List
|
||||||
deferredMeasurementCache={this.cellSizeCache}
|
deferredMeasurementCache={this.cellSizeCache}
|
||||||
height={height}
|
height={height}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
onScroll={this.onScroll as any}
|
onScroll={this.onScroll as any}
|
||||||
overscanRowCount={10}
|
overscanRowCount={10}
|
||||||
ref={this.listRef}
|
ref={this.listRef}
|
||||||
|
|
|
@ -2,13 +2,10 @@ import * as React from 'react';
|
||||||
|
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
|
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
@ -80,7 +77,6 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
||||||
|
|
||||||
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||||
})
|
})
|
||||||
// tslint:disable-next-line max-func-body-length
|
|
||||||
.add('Notification', () => {
|
.add('Notification', () => {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
|
@ -173,7 +169,7 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
||||||
acceptedTime: Date.now() - 200,
|
acceptedTime: Date.now() - 200,
|
||||||
wasDeclined: false,
|
wasDeclined: false,
|
||||||
wasIncoming: false,
|
wasIncoming: false,
|
||||||
wasVideoCall: true,
|
wasVideoCall: false,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -193,8 +189,8 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
callHistoryDetails: {
|
|
||||||
data: {
|
data: {
|
||||||
|
callHistoryDetails: {
|
||||||
// declined outgoing audio
|
// declined outgoing audio
|
||||||
wasDeclined: true,
|
wasDeclined: true,
|
||||||
wasIncoming: false,
|
wasIncoming: false,
|
||||||
|
@ -243,20 +239,21 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{items.map(item => (
|
{items.map((item, index) => (
|
||||||
<>
|
<React.Fragment key={index}>
|
||||||
<TimelineItem
|
<TimelineItem
|
||||||
{...getDefaultProps()}
|
{...getDefaultProps()}
|
||||||
item={item as TimelineItemProps['item']}
|
item={item as TimelineItemProps['item']}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
</>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.add('Unknown Type', () => {
|
.add('Unknown Type', () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: intentional
|
// @ts-ignore: intentional
|
||||||
const item = {
|
const item = {
|
||||||
type: 'random',
|
type: 'random',
|
||||||
|
@ -268,6 +265,7 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
||||||
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||||
})
|
})
|
||||||
.add('Missing Item', () => {
|
.add('Missing Item', () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: intentional
|
// @ts-ignore: intentional
|
||||||
const item = null as TimelineItemProps['item'];
|
const item = null as TimelineItemProps['item'];
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,7 @@ export type PropsType = PropsLocalType &
|
||||||
Pick<AllMessageProps, 'renderEmojiPicker'>;
|
Pick<AllMessageProps, 'renderEmojiPicker'>;
|
||||||
|
|
||||||
export class TimelineItem extends React.PureComponent<PropsType> {
|
export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
public render() {
|
public render(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
conversationId,
|
conversationId,
|
||||||
id,
|
id,
|
||||||
|
@ -136,8 +136,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
// tslint:disable-next-line:no-console
|
window.log.warn(`TimelineItem: item ${id} provided was falsey`);
|
||||||
console.warn(`TimelineItem: item ${id} provided was falsey`);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,18 +16,15 @@ export type Props = {
|
||||||
const FAKE_DURATION = 1000;
|
const FAKE_DURATION = 1000;
|
||||||
|
|
||||||
export class TimelineLoadingRow extends React.PureComponent<Props> {
|
export class TimelineLoadingRow extends React.PureComponent<Props> {
|
||||||
public renderContents() {
|
public renderContents(): JSX.Element {
|
||||||
const { state, duration, expiresAt, onComplete } = this.props;
|
const { state, duration, expiresAt, onComplete } = this.props;
|
||||||
|
|
||||||
if (state === 'idle') {
|
if (state === 'idle') {
|
||||||
const fakeExpiresAt = Date.now() - FAKE_DURATION;
|
const fakeExpiresAt = Date.now() - FAKE_DURATION;
|
||||||
|
|
||||||
return <Countdown duration={FAKE_DURATION} expiresAt={fakeExpiresAt} />;
|
return <Countdown duration={FAKE_DURATION} expiresAt={fakeExpiresAt} />;
|
||||||
} else if (
|
}
|
||||||
state === 'countdown' &&
|
if (state === 'countdown' && isNumber(duration) && isNumber(expiresAt)) {
|
||||||
isNumber(duration) &&
|
|
||||||
isNumber(expiresAt)
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Countdown
|
<Countdown
|
||||||
duration={duration}
|
duration={duration}
|
||||||
|
@ -40,7 +37,7 @@ export class TimelineLoadingRow extends React.PureComponent<Props> {
|
||||||
return <Spinner size="24" svgSize="small" direction="on-background" />;
|
return <Spinner size="24" svgSize="small" direction="on-background" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="module-timeline-loading-row">{this.renderContents()}</div>
|
<div className="module-timeline-loading-row">{this.renderContents()}</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,12 +2,8 @@ import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { boolean, select, text } from '@storybook/addon-knobs';
|
import { boolean, select, text } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Props, TimerNotification } from './TimerNotification';
|
import { Props, TimerNotification } from './TimerNotification';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
|
@ -22,7 +22,7 @@ type PropsHousekeeping = {
|
||||||
export type Props = PropsData & PropsHousekeeping;
|
export type Props = PropsData & PropsHousekeeping;
|
||||||
|
|
||||||
export class TimerNotification extends React.Component<Props> {
|
export class TimerNotification extends React.Component<Props> {
|
||||||
public renderContents() {
|
public renderContents(): JSX.Element | string | null {
|
||||||
const {
|
const {
|
||||||
i18n,
|
i18n,
|
||||||
name,
|
name,
|
||||||
|
@ -71,13 +71,13 @@ export class TimerNotification extends React.Component<Props> {
|
||||||
? i18n('disappearingMessagesDisabledByMember')
|
? i18n('disappearingMessagesDisabledByMember')
|
||||||
: i18n('timerSetByMember', [timespan]);
|
: i18n('timerSetByMember', [timespan]);
|
||||||
default:
|
default:
|
||||||
console.warn('TimerNotification: unsupported type provided:', type);
|
window.log.warn('TimerNotification: unsupported type provided:', type);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { timespan, disabled } = this.props;
|
const { timespan, disabled } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,19 +2,15 @@ import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { boolean, date, select, text } from '@storybook/addon-knobs';
|
import { boolean, date, select, text } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Props, Timestamp } from './Timestamp';
|
import { Props, Timestamp } from './Timestamp';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/Timestamp', module);
|
const story = storiesOf('Components/Conversation/Timestamp', module);
|
||||||
|
|
||||||
const now = Date.now;
|
const { now } = Date;
|
||||||
const seconds = (n: number) => n * 1000;
|
const seconds = (n: number) => n * 1000;
|
||||||
const minutes = (n: number) => 60 * seconds(n);
|
const minutes = (n: number) => 60 * seconds(n);
|
||||||
const hours = (n: number) => 60 * minutes(n);
|
const hours = (n: number) => 60 * minutes(n);
|
||||||
|
@ -70,6 +66,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
|
|
||||||
const createTable = (overrideProps: Partial<Props> = {}) => (
|
const createTable = (overrideProps: Partial<Props> = {}) => (
|
||||||
<table cellPadding={5}>
|
<table cellPadding={5}>
|
||||||
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Timestamp</th>
|
<th>Timestamp</th>
|
||||||
|
@ -85,6 +82,7 @@ const createTable = (overrideProps: Partial<Props> = {}) => (
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export interface Props {
|
||||||
const UPDATE_FREQUENCY = 60 * 1000;
|
const UPDATE_FREQUENCY = 60 * 1000;
|
||||||
|
|
||||||
export class Timestamp extends React.Component<Props> {
|
export class Timestamp extends React.Component<Props> {
|
||||||
private interval: any;
|
private interval: NodeJS.Timeout | null;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -29,22 +29,24 @@ export class Timestamp extends React.Component<Props> {
|
||||||
this.interval = null;
|
this.interval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount(): void {
|
||||||
const update = () => {
|
const update = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
// Used to trigger renders
|
||||||
|
// eslint-disable-next-line react/no-unused-state
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
this.interval = setInterval(update, UPDATE_FREQUENCY);
|
this.interval = setInterval(update, UPDATE_FREQUENCY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Props, TypingAnimation } from './TypingAnimation';
|
import { Props, TypingAnimation } from './TypingAnimation';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
|
@ -8,11 +8,7 @@ export interface Props {
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TypingAnimation extends React.Component<Props> {
|
export const TypingAnimation = ({ i18n, color }: Props): JSX.Element => (
|
||||||
public render() {
|
|
||||||
const { i18n, color } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="module-typing-animation" title={i18n('typingAlt')}>
|
<div className="module-typing-animation" title={i18n('typingAlt')}>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -39,5 +35,3 @@ export class TypingAnimation extends React.Component<Props> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,12 +2,8 @@ import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { select, text } from '@storybook/addon-knobs';
|
import { select, text } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Props, TypingBubble } from './TypingBubble';
|
import { Props, TypingBubble } from './TypingBubble';
|
||||||
import { Colors } from '../../types/Colors';
|
import { Colors } from '../../types/Colors';
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TypingBubble extends React.PureComponent<Props> {
|
export class TypingBubble extends React.PureComponent<Props> {
|
||||||
public renderAvatar() {
|
public renderAvatar(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
avatarPath,
|
avatarPath,
|
||||||
color,
|
color,
|
||||||
|
@ -32,7 +32,7 @@ export class TypingBubble extends React.PureComponent<Props> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (conversationType !== 'group') {
|
if (conversationType !== 'group') {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -52,7 +52,7 @@ export class TypingBubble extends React.PureComponent<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { i18n, color, conversationType } = this.props;
|
const { i18n, color, conversationType } = this.props;
|
||||||
const isGroup = conversationType === 'group';
|
const isGroup = conversationType === 'group';
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,8 @@ import { storiesOf } from '@storybook/react';
|
||||||
import { boolean, text } from '@storybook/addon-knobs';
|
import { boolean, text } from '@storybook/addon-knobs';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { ContactType, Props, UnsupportedMessage } from './UnsupportedMessage';
|
import { ContactType, Props, UnsupportedMessage } from './UnsupportedMessage';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
|
@ -29,9 +29,12 @@ type PropsHousekeeping = {
|
||||||
|
|
||||||
export type Props = PropsData & PropsHousekeeping & PropsActions;
|
export type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||||
|
|
||||||
export class UnsupportedMessage extends React.Component<Props> {
|
export const UnsupportedMessage = ({
|
||||||
public render() {
|
canProcessNow,
|
||||||
const { canProcessNow, contact, i18n, downloadNewVersion } = this.props;
|
contact,
|
||||||
|
i18n,
|
||||||
|
downloadNewVersion,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
const { isMe } = contact;
|
const { isMe } = contact;
|
||||||
|
|
||||||
const otherStringId = canProcessNow
|
const otherStringId = canProcessNow
|
||||||
|
@ -47,9 +50,7 @@ export class UnsupportedMessage extends React.Component<Props> {
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-unsupported-message__icon',
|
'module-unsupported-message__icon',
|
||||||
canProcessNow
|
canProcessNow ? 'module-unsupported-message__icon--can-process' : null
|
||||||
? 'module-unsupported-message__icon--can-process'
|
|
||||||
: null
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="module-unsupported-message__text">
|
<div className="module-unsupported-message__text">
|
||||||
|
@ -75,6 +76,7 @@ export class UnsupportedMessage extends React.Component<Props> {
|
||||||
</div>
|
</div>
|
||||||
{canProcessNow ? null : (
|
{canProcessNow ? null : (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
downloadNewVersion();
|
downloadNewVersion();
|
||||||
}}
|
}}
|
||||||
|
@ -85,5 +87,4 @@ export class UnsupportedMessage extends React.Component<Props> {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { boolean } from '@storybook/addon-knobs';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Props, VerificationNotification } from './VerificationNotification';
|
import { Props, VerificationNotification } from './VerificationNotification';
|
||||||
import { boolean } from '@storybook/addon-knobs';
|
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ type PropsHousekeeping = {
|
||||||
export type Props = PropsData & PropsHousekeeping;
|
export type Props = PropsData & PropsHousekeeping;
|
||||||
|
|
||||||
export class VerificationNotification extends React.Component<Props> {
|
export class VerificationNotification extends React.Component<Props> {
|
||||||
public getStringId() {
|
public getStringId(): string {
|
||||||
const { isLocal, type } = this.props;
|
const { isLocal, type } = this.props;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -44,7 +44,7 @@ export class VerificationNotification extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderContents() {
|
public renderContents(): JSX.Element {
|
||||||
const { contact, i18n } = this.props;
|
const { contact, i18n } = this.props;
|
||||||
const id = this.getStringId();
|
const id = this.getStringId();
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ export class VerificationNotification extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { type } = this.props;
|
const { type } = this.props;
|
||||||
const suffix =
|
const suffix =
|
||||||
type === 'markVerified' ? 'mark-verified' : 'mark-not-verified';
|
type === 'markVerified' ? 'mark-verified' : 'mark-not-verified';
|
||||||
|
|
|
@ -19,7 +19,7 @@ export function renderAvatar({
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
size: 28 | 52 | 80;
|
size: 28 | 52 | 80;
|
||||||
direction?: 'outgoing' | 'incoming';
|
direction?: 'outgoing' | 'incoming';
|
||||||
}) {
|
}): JSX.Element {
|
||||||
const { avatar } = contact;
|
const { avatar } = contact;
|
||||||
|
|
||||||
const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
|
const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
|
||||||
|
@ -60,7 +60,7 @@ export function renderName({
|
||||||
contact: ContactType;
|
contact: ContactType;
|
||||||
isIncoming: boolean;
|
isIncoming: boolean;
|
||||||
module: string;
|
module: string;
|
||||||
}) {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -81,7 +81,7 @@ export function renderContactShorthand({
|
||||||
contact: ContactType;
|
contact: ContactType;
|
||||||
isIncoming: boolean;
|
isIncoming: boolean;
|
||||||
module: string;
|
module: string;
|
||||||
}) {
|
}): JSX.Element {
|
||||||
const { number: phoneNumber, email } = contact;
|
const { number: phoneNumber, email } = contact;
|
||||||
const firstNumber = phoneNumber && phoneNumber[0] && phoneNumber[0].value;
|
const firstNumber = phoneNumber && phoneNumber[0] && phoneNumber[0].value;
|
||||||
const firstEmail = email && email[0] && email[0].value;
|
const firstEmail = email && email[0] && email[0].value;
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { select, text, withKnobs } from '@storybook/addon-knobs';
|
import { select, text, withKnobs } from '@storybook/addon-knobs';
|
||||||
import { random, range, sample, sortBy } from 'lodash';
|
import { random, range, sample, sortBy } from 'lodash';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../../_locales/en/messages.json';
|
import enMessages from '../../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { MIMEType } from '../../../types/MIME';
|
import { MIMEType } from '../../../types/MIME';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../LightboxGallery';
|
||||||
|
|
||||||
|
@ -20,6 +19,7 @@ const story = storiesOf(
|
||||||
module
|
module
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
||||||
|
|
||||||
export const now = Date.now();
|
export const now = Date.now();
|
||||||
|
|
|
@ -16,7 +16,7 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AttachmentSection extends React.Component<Props> {
|
export class AttachmentSection extends React.Component<Props> {
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { header } = this.props;
|
const { header } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -10,6 +10,7 @@ const story = storiesOf(
|
||||||
module
|
module
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
||||||
|
|
||||||
story.add('Single', () => (
|
story.add('Single', () => (
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
// tslint:disable-next-line:match-default-export-name
|
|
||||||
import formatFileSize from 'filesize';
|
import formatFileSize from 'filesize';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -21,7 +20,7 @@ export class DocumentListItem extends React.Component<Props> {
|
||||||
shouldShowSeparator: true,
|
shouldShowSeparator: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { shouldShowSeparator } = this.props;
|
const { shouldShowSeparator } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -39,12 +38,13 @@ export class DocumentListItem extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderContent() {
|
private renderContent() {
|
||||||
const { fileName, fileSize, timestamp } = this.props;
|
const { fileName, fileSize, onClick, timestamp } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="module-document-list-item__content"
|
className="module-document-list-item__content"
|
||||||
onClick={this.props.onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="module-document-list-item__icon" />
|
<div className="module-document-list-item__icon" />
|
||||||
<div className="module-document-list-item__metadata">
|
<div className="module-document-list-item__metadata">
|
||||||
|
|
|
@ -8,6 +8,7 @@ const story = storiesOf(
|
||||||
module
|
module
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
||||||
|
|
||||||
story.add('Default', () => {
|
story.add('Default', () => {
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
/**
|
|
||||||
* @prettier
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EmptyState extends React.Component<Props> {
|
export const EmptyState = ({ label }: Props): JSX.Element => (
|
||||||
public render() {
|
<div className="module-empty-state">{label}</div>
|
||||||
const { label } = this.props;
|
);
|
||||||
|
|
||||||
return <div className="module-empty-state">{label}</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export const LoadingIndicator = () => {
|
export const LoadingIndicator = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="loading-widget">
|
<div className="loading-widget">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
|
|
@ -2,9 +2,7 @@ import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../../_locales/en/messages.json';
|
import enMessages from '../../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -48,6 +48,8 @@ const Tab = ({
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// Has key events handled elsewhere
|
||||||
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-media-gallery__tab',
|
'module-media-gallery__tab',
|
||||||
|
@ -64,11 +66,15 @@ const Tab = ({
|
||||||
|
|
||||||
export class MediaGallery extends React.Component<Props, State> {
|
export class MediaGallery extends React.Component<Props, State> {
|
||||||
public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();
|
public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||||
public state: State = {
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
selectedTab: 'media',
|
selectedTab: 'media',
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount(): void {
|
||||||
// When this component is created, it's initially not part of the DOM, and then it's
|
// When this component is created, it's initially not part of the DOM, and then it's
|
||||||
// added off-screen and animated in. This ensures that the focus takes.
|
// added off-screen and animated in. This ensures that the focus takes.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -78,7 +84,7 @@ export class MediaGallery extends React.Component<Props, State> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
const { selectedTab } = this.state;
|
const { selectedTab } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -3,11 +3,8 @@ import { storiesOf } from '@storybook/react';
|
||||||
import { text, withKnobs } from '@storybook/addon-knobs';
|
import { text, withKnobs } from '@storybook/addon-knobs';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
|
||||||
import enMessages from '../../../../_locales/en/messages.json';
|
import enMessages from '../../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../LightboxGallery';
|
||||||
import { AttachmentType } from '../../../types/Attachment';
|
import { AttachmentType } from '../../../types/Attachment';
|
||||||
import { MIMEType } from '../../../types/MIME';
|
import { MIMEType } from '../../../types/MIME';
|
||||||
|
@ -22,6 +19,7 @@ const story = storiesOf(
|
||||||
module
|
module
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
||||||
|
|
||||||
const createProps = (
|
const createProps = (
|
||||||
|
|
|
@ -31,9 +31,8 @@ export class MediaGridItem extends React.Component<Props, State> {
|
||||||
this.onImageErrorBound = this.onImageError.bind(this);
|
this.onImageErrorBound = this.onImageError.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onImageError() {
|
public onImageError(): void {
|
||||||
// tslint:disable-next-line no-console
|
window.log.info(
|
||||||
console.log(
|
|
||||||
'MediaGridItem: Image failed to load; failing over to placeholder'
|
'MediaGridItem: Image failed to load; failing over to placeholder'
|
||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -41,7 +40,7 @@ export class MediaGridItem extends React.Component<Props, State> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderContent() {
|
public renderContent(): JSX.Element | null {
|
||||||
const { mediaItem, i18n } = this.props;
|
const { mediaItem, i18n } = this.props;
|
||||||
const { imageBroken } = this.state;
|
const { imageBroken } = this.state;
|
||||||
const { attachment, contentType } = mediaItem;
|
const { attachment, contentType } = mediaItem;
|
||||||
|
@ -70,7 +69,8 @@ export class MediaGridItem extends React.Component<Props, State> {
|
||||||
onError={this.onImageErrorBound}
|
onError={this.onImageErrorBound}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (contentType && isVideoTypeSupported(contentType)) {
|
}
|
||||||
|
if (contentType && isVideoTypeSupported(contentType)) {
|
||||||
if (imageBroken || !mediaItem.thumbnailObjectUrl) {
|
if (imageBroken || !mediaItem.thumbnailObjectUrl) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -107,9 +107,15 @@ export class MediaGridItem extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render(): JSX.Element {
|
||||||
|
const { onClick } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className="module-media-grid-item" onClick={this.props.onClick}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="module-media-grid-item"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
{this.renderContent()}
|
{this.renderContent()}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -67,11 +67,13 @@ const toSection = (
|
||||||
case 'yesterday':
|
case 'yesterday':
|
||||||
case 'thisWeek':
|
case 'thisWeek':
|
||||||
case 'thisMonth':
|
case 'thisMonth':
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return {
|
return {
|
||||||
type: firstMediaItemWithSection.type,
|
type: firstMediaItemWithSection.type,
|
||||||
mediaItems,
|
mediaItems,
|
||||||
};
|
};
|
||||||
case 'yearMonth':
|
case 'yearMonth':
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
return {
|
return {
|
||||||
type: firstMediaItemWithSection.type,
|
type: firstMediaItemWithSection.type,
|
||||||
year: firstMediaItemWithSection.year,
|
year: firstMediaItemWithSection.year,
|
||||||
|
@ -83,6 +85,7 @@ const toSection = (
|
||||||
// error TS2345: Argument of type 'any' is not assignable to parameter
|
// error TS2345: Argument of type 'any' is not assignable to parameter
|
||||||
// of type 'never'.
|
// of type 'never'.
|
||||||
// return missingCaseError(firstMediaItemWithSection.type);
|
// return missingCaseError(firstMediaItemWithSection.type);
|
||||||
|
// eslint-disable-next-line no-useless-return
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,5 +3,7 @@ import { Attachment } from '../../../../types/Attachment';
|
||||||
export type Message = {
|
export type Message = {
|
||||||
id: string;
|
id: string;
|
||||||
attachments: Array<Attachment>;
|
attachments: Array<Attachment>;
|
||||||
|
// Assuming this is for the API
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
received_at: number;
|
received_at: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -12830,18 +12830,17 @@
|
||||||
"path": "ts/components/CallScreen.js",
|
"path": "ts/components/CallScreen.js",
|
||||||
"line": " this.localVideoRef = react_1.default.createRef();",
|
"line": " this.localVideoRef = react_1.default.createRef();",
|
||||||
"lineNumber": 98,
|
"lineNumber": 98,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
||||||
"updated": "2020-05-28T17:22:06.472Z",
|
"updated": "2020-09-14T23:03:44.863Z",
|
||||||
"reasonDetail": "Used to render local preview video"
|
"reasonDetail": "<optional>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/CallScreen.js",
|
"path": "ts/components/CallScreen.js",
|
||||||
"line": " this.remoteVideoRef = react_1.default.createRef();",
|
"line": " this.remoteVideoRef = react_1.default.createRef();",
|
||||||
"lineNumber": 98,
|
"lineNumber": 99,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-09-11T17:24:56.124Z",
|
"updated": "2020-09-14T23:03:44.863Z"
|
||||||
"reasonDetail": "Necessary for showing call video"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
|
@ -12856,10 +12855,9 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/CallScreen.tsx",
|
"path": "ts/components/CallScreen.tsx",
|
||||||
"line": " this.remoteVideoRef = React.createRef();",
|
"line": " this.remoteVideoRef = React.createRef();",
|
||||||
"lineNumber": 75,
|
"lineNumber": 80,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-09-11T17:24:56.124Z",
|
"updated": "2020-09-14T23:03:44.863Z"
|
||||||
"reasonDetail": "Necessary for showing call video"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
|
@ -12944,10 +12942,9 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/Lightbox.js",
|
"path": "ts/components/Lightbox.js",
|
||||||
"line": " this.videoRef = react_1.default.createRef();",
|
"line": " this.videoRef = react_1.default.createRef();",
|
||||||
"lineNumber": 142,
|
"lineNumber": 149,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-09-11T17:24:56.124Z",
|
"updated": "2020-09-14T23:03:44.863Z"
|
||||||
"reasonDetail": "Used to control video"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
|
@ -13016,7 +13013,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/ConversationHeader.tsx",
|
"path": "ts/components/conversation/ConversationHeader.tsx",
|
||||||
"line": " this.menuTriggerRef = React.createRef();",
|
"line": " this.menuTriggerRef = React.createRef();",
|
||||||
"lineNumber": 79,
|
"lineNumber": 82,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-20T20:10:43.540Z",
|
"updated": "2020-05-20T20:10:43.540Z",
|
||||||
"reasonDetail": "Used to reference popup menu"
|
"reasonDetail": "Used to reference popup menu"
|
||||||
|
@ -13069,24 +13066,23 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
||||||
"lineNumber": 212,
|
"lineNumber": 213,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-08-28T19:36:40.817Z"
|
"updated": "2020-09-14T23:03:44.863Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||||
"lineNumber": 213,
|
"lineNumber": 215,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-09-11T17:24:56.124Z",
|
"updated": "2020-09-14T23:03:44.863Z"
|
||||||
"reasonDetail": "Used for managing focus only"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " > = React.createRef();",
|
"line": " > = React.createRef();",
|
||||||
"lineNumber": 216,
|
"lineNumber": 219,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-08-28T19:36:40.817Z"
|
"updated": "2020-08-28T19:36:40.817Z"
|
||||||
},
|
},
|
||||||
|
@ -13094,7 +13090,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/MessageDetail.js",
|
"path": "ts/components/conversation/MessageDetail.js",
|
||||||
"line": " this.focusRef = react_1.default.createRef();",
|
"line": " this.focusRef = react_1.default.createRef();",
|
||||||
"lineNumber": 15,
|
"lineNumber": 18,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-01T22:46:33.013Z",
|
"updated": "2019-11-01T22:46:33.013Z",
|
||||||
"reasonDetail": "Used for setting focus only"
|
"reasonDetail": "Used for setting focus only"
|
||||||
|
@ -13112,7 +13108,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/media-gallery/MediaGallery.js",
|
"path": "ts/components/conversation/media-gallery/MediaGallery.js",
|
||||||
"line": " this.focusRef = react_1.default.createRef();",
|
"line": " this.focusRef = react_1.default.createRef();",
|
||||||
"lineNumber": 25,
|
"lineNumber": 28,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-01T22:46:33.013Z",
|
"updated": "2019-11-01T22:46:33.013Z",
|
||||||
"reasonDetail": "Used for setting focus only"
|
"reasonDetail": "Used for setting focus only"
|
||||||
|
@ -13121,7 +13117,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
|
"path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
|
||||||
"line": " public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
"line": " public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||||
"lineNumber": 66,
|
"lineNumber": 68,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-01T22:46:33.013Z",
|
"updated": "2019-11-01T22:46:33.013Z",
|
||||||
"reasonDetail": "Used for setting focus only"
|
"reasonDetail": "Used for setting focus only"
|
||||||
|
|
|
@ -181,6 +181,7 @@
|
||||||
"ts/backbone/**",
|
"ts/backbone/**",
|
||||||
"ts/build/**",
|
"ts/build/**",
|
||||||
"ts/components/*.ts[x]",
|
"ts/components/*.ts[x]",
|
||||||
|
"ts/components/conversation/**",
|
||||||
"ts/components/emoji/**",
|
"ts/components/emoji/**",
|
||||||
"ts/notifications/**",
|
"ts/notifications/**",
|
||||||
"ts/protobuf/**",
|
"ts/protobuf/**",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue