Migrate most React class components to function components
This commit is contained in:
parent
4c9baaef80
commit
558b5a4a38
23 changed files with 1444 additions and 1775 deletions
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactChild, ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
|
@ -106,22 +106,54 @@ const _keyForError = (error: Error): string => {
|
|||
return `${error.name}-${error.message}`;
|
||||
};
|
||||
|
||||
export class MessageDetail extends React.Component<Props> {
|
||||
private readonly focusRef = React.createRef<HTMLDivElement>();
|
||||
private readonly messageContainerRef = React.createRef<HTMLDivElement>();
|
||||
export function MessageDetail({
|
||||
contacts,
|
||||
errors,
|
||||
message,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
checkForAccount,
|
||||
clearTargetedMessage,
|
||||
contactNameColor,
|
||||
doubleCheckMissingQuoteReference,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
interactionMode,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
messageExpanded,
|
||||
openGiftBadge,
|
||||
platform,
|
||||
pushPanelForConversation,
|
||||
renderAudioAttachment,
|
||||
saveAttachment,
|
||||
showContactModal,
|
||||
showConversation,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showLightbox,
|
||||
showLightboxForViewOnceMedia,
|
||||
showSpoiler,
|
||||
startConversation,
|
||||
theme,
|
||||
toggleSafetyNumberModal,
|
||||
viewStory,
|
||||
}: Props): JSX.Element {
|
||||
const focusRef = useRef<HTMLDivElement>(null);
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
public override componentDidMount(): void {
|
||||
// When this component is created, it's initially not part of the DOM, and then it's
|
||||
// added off-screen and animated in. This ensures that the focus takes.
|
||||
setTimeout(() => {
|
||||
if (this.focusRef.current) {
|
||||
this.focusRef.current.focus();
|
||||
}
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
// 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.
|
||||
focusRef.current?.focus();
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
public renderAvatar(contact: Contact): JSX.Element {
|
||||
const { getPreferredBadge, i18n, theme } = this.props;
|
||||
function renderAvatar(contact: Contact): JSX.Element {
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
|
@ -155,9 +187,8 @@ export class MessageDetail extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderContact(contact: Contact): JSX.Element {
|
||||
const { i18n, toggleSafetyNumberModal } = this.props;
|
||||
const errors = contact.errors || [];
|
||||
function renderContact(contact: Contact): JSX.Element {
|
||||
const contactErrors = contact.errors || [];
|
||||
|
||||
const errorComponent = contact.isOutgoingKeyError ? (
|
||||
<div className="module-message-detail__contact__error-buttons">
|
||||
|
@ -176,17 +207,17 @@ export class MessageDetail extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<div key={contact.id} className="module-message-detail__contact">
|
||||
{this.renderAvatar(contact)}
|
||||
{renderAvatar(contact)}
|
||||
<div className="module-message-detail__contact__text">
|
||||
<div className="module-message-detail__contact__name">
|
||||
<ContactName title={contact.title} />
|
||||
</div>
|
||||
{errors.map(error => (
|
||||
{contactErrors.map(contactError => (
|
||||
<div
|
||||
key={_keyForError(error)}
|
||||
key={_keyForError(contactError)}
|
||||
className="module-message-detail__contact__error"
|
||||
>
|
||||
{error.message}
|
||||
{contactError.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -204,11 +235,9 @@ export class MessageDetail extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
private renderContactGroupHeaderText(
|
||||
function renderContactGroupHeaderText(
|
||||
sendStatus: undefined | SendStatus
|
||||
): string {
|
||||
const { i18n } = this.props;
|
||||
|
||||
if (sendStatus === undefined) {
|
||||
return i18n('icu:from');
|
||||
}
|
||||
|
@ -231,19 +260,19 @@ export class MessageDetail extends React.Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
private renderContactGroup(
|
||||
function renderContactGroup(
|
||||
sendStatus: undefined | SendStatus,
|
||||
contacts: undefined | ReadonlyArray<Contact>
|
||||
statusContacts: undefined | ReadonlyArray<Contact>
|
||||
): ReactNode {
|
||||
if (!contacts || !contacts.length) {
|
||||
if (!statusContacts || !statusContacts.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortedContacts = [...contacts].sort((a, b) =>
|
||||
const sortedContacts = [...statusContacts].sort((a, b) =>
|
||||
contactSortCollator.compare(a.title, b.title)
|
||||
);
|
||||
|
||||
const headerText = this.renderContactGroupHeaderText(sendStatus);
|
||||
const headerText = renderContactGroupHeaderText(sendStatus);
|
||||
|
||||
return (
|
||||
<div key={headerText} className="module-message-detail__contact-group">
|
||||
|
@ -256,16 +285,14 @@ export class MessageDetail extends React.Component<Props> {
|
|||
>
|
||||
{headerText}
|
||||
</div>
|
||||
{sortedContacts.map(contact => this.renderContact(contact))}
|
||||
{sortedContacts.map(contact => renderContact(contact))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderContacts(): ReactChild {
|
||||
function renderContacts(): ReactChild {
|
||||
// This assumes that the list either contains one sender (a status of `undefined`) or
|
||||
// 1+ contacts with `SendStatus`es, but it doesn't check that assumption.
|
||||
const { contacts } = this.props;
|
||||
|
||||
const contactsBySendStatus = groupBy(contacts, contact => contact.status);
|
||||
|
||||
return (
|
||||
|
@ -279,181 +306,135 @@ export class MessageDetail extends React.Component<Props> {
|
|||
SendStatus.Sent,
|
||||
SendStatus.Pending,
|
||||
].map(sendStatus =>
|
||||
this.renderContactGroup(
|
||||
sendStatus,
|
||||
contactsBySendStatus.get(sendStatus)
|
||||
)
|
||||
renderContactGroup(sendStatus, contactsBySendStatus.get(sendStatus))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public override render(): JSX.Element {
|
||||
const {
|
||||
errors,
|
||||
message,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
const timeRemaining = message.expirationTimestamp
|
||||
? DurationInSeconds.fromMillis(message.expirationTimestamp - Date.now())
|
||||
: undefined;
|
||||
|
||||
checkForAccount,
|
||||
clearTargetedMessage,
|
||||
contactNameColor,
|
||||
doubleCheckMissingQuoteReference,
|
||||
getPreferredBadge,
|
||||
i18n,
|
||||
interactionMode,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
messageExpanded,
|
||||
openGiftBadge,
|
||||
platform,
|
||||
pushPanelForConversation,
|
||||
renderAudioAttachment,
|
||||
saveAttachment,
|
||||
showContactModal,
|
||||
showConversation,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showLightbox,
|
||||
showLightboxForViewOnceMedia,
|
||||
showSpoiler,
|
||||
startConversation,
|
||||
theme,
|
||||
viewStory,
|
||||
} = this.props;
|
||||
|
||||
const timeRemaining = message.expirationTimestamp
|
||||
? DurationInSeconds.fromMillis(message.expirationTimestamp - Date.now())
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
|
||||
<div
|
||||
className="module-message-detail__message-container"
|
||||
ref={this.messageContainerRef}
|
||||
>
|
||||
<Message
|
||||
{...message}
|
||||
renderingContext="conversation/MessageDetail"
|
||||
checkForAccount={checkForAccount}
|
||||
clearTargetedMessage={clearTargetedMessage}
|
||||
contactNameColor={contactNameColor}
|
||||
containerElementRef={this.messageContainerRef}
|
||||
containerWidthBreakpoint={WidthBreakpoint.Wide}
|
||||
renderMenu={undefined}
|
||||
disableScroll
|
||||
displayLimit={Number.MAX_SAFE_INTEGER}
|
||||
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
|
||||
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
interactionMode={interactionMode}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||
messageExpanded={messageExpanded}
|
||||
openGiftBadge={openGiftBadge}
|
||||
platform={platform}
|
||||
pushPanelForConversation={pushPanelForConversation}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
saveAttachment={saveAttachment}
|
||||
shouldCollapseAbove={false}
|
||||
shouldCollapseBelow={false}
|
||||
shouldHideMetadata={false}
|
||||
showConversation={showConversation}
|
||||
showSpoiler={showSpoiler}
|
||||
scrollToQuotedMessage={() => {
|
||||
log.warn('MessageDetail: scrollToQuotedMessage called!');
|
||||
}}
|
||||
showContactModal={showContactModal}
|
||||
showExpiredIncomingTapToViewToast={
|
||||
showExpiredIncomingTapToViewToast
|
||||
}
|
||||
showExpiredOutgoingTapToViewToast={
|
||||
showExpiredOutgoingTapToViewToast
|
||||
}
|
||||
showLightbox={showLightbox}
|
||||
startConversation={startConversation}
|
||||
theme={theme}
|
||||
viewStory={viewStory}
|
||||
onToggleSelect={noop}
|
||||
onReplyToMessage={noop}
|
||||
/>
|
||||
</div>
|
||||
<table className="module-message-detail__info">
|
||||
<tbody>
|
||||
{(errors || []).map(error => (
|
||||
<tr key={_keyForError(error)}>
|
||||
<td className="module-message-detail__label">
|
||||
{i18n('icu:error')}
|
||||
</td>
|
||||
<td>
|
||||
{' '}
|
||||
<span className="error-message">{error.message}</span>{' '}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
<div className="module-message-detail" tabIndex={0} ref={focusRef}>
|
||||
<div
|
||||
className="module-message-detail__message-container"
|
||||
ref={messageContainerRef}
|
||||
>
|
||||
<Message
|
||||
{...message}
|
||||
renderingContext="conversation/MessageDetail"
|
||||
checkForAccount={checkForAccount}
|
||||
clearTargetedMessage={clearTargetedMessage}
|
||||
contactNameColor={contactNameColor}
|
||||
containerElementRef={messageContainerRef}
|
||||
containerWidthBreakpoint={WidthBreakpoint.Wide}
|
||||
renderMenu={undefined}
|
||||
disableScroll
|
||||
displayLimit={Number.MAX_SAFE_INTEGER}
|
||||
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
|
||||
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
interactionMode={interactionMode}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||
messageExpanded={messageExpanded}
|
||||
openGiftBadge={openGiftBadge}
|
||||
platform={platform}
|
||||
pushPanelForConversation={pushPanelForConversation}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
saveAttachment={saveAttachment}
|
||||
shouldCollapseAbove={false}
|
||||
shouldCollapseBelow={false}
|
||||
shouldHideMetadata={false}
|
||||
showConversation={showConversation}
|
||||
showSpoiler={showSpoiler}
|
||||
scrollToQuotedMessage={() => {
|
||||
log.warn('MessageDetail: scrollToQuotedMessage called!');
|
||||
}}
|
||||
showContactModal={showContactModal}
|
||||
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
|
||||
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
|
||||
showLightbox={showLightbox}
|
||||
startConversation={startConversation}
|
||||
theme={theme}
|
||||
viewStory={viewStory}
|
||||
onToggleSelect={noop}
|
||||
onReplyToMessage={noop}
|
||||
/>
|
||||
</div>
|
||||
<table className="module-message-detail__info">
|
||||
<tbody>
|
||||
{(errors || []).map(error => (
|
||||
<tr key={_keyForError(error)}>
|
||||
<td className="module-message-detail__label">
|
||||
{i18n('icu:sent')}
|
||||
{i18n('icu:error')}
|
||||
</td>
|
||||
<td>
|
||||
<ContextMenu
|
||||
i18n={i18n}
|
||||
menuOptions={[
|
||||
{
|
||||
icon: 'StoryDetailsModal__copy-icon',
|
||||
label: i18n('icu:StoryDetailsModal__copy-timestamp'),
|
||||
onClick: () => {
|
||||
void window.navigator.clipboard.writeText(
|
||||
String(sentAt)
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<>
|
||||
<Time timestamp={sentAt}>
|
||||
{formatDateTimeLong(i18n, sentAt)}
|
||||
</Time>{' '}
|
||||
<span className="module-message-detail__unix-timestamp">
|
||||
({sentAt})
|
||||
</span>
|
||||
</>
|
||||
</ContextMenu>
|
||||
{' '}
|
||||
<span className="error-message">{error.message}</span>{' '}
|
||||
</td>
|
||||
</tr>
|
||||
{receivedAt && message.direction === 'incoming' ? (
|
||||
<tr>
|
||||
<td className="module-message-detail__label">
|
||||
{i18n('icu:received')}
|
||||
</td>
|
||||
<td>
|
||||
<Time timestamp={receivedAt}>
|
||||
{formatDateTimeLong(i18n, receivedAt)}
|
||||
))}
|
||||
<tr>
|
||||
<td className="module-message-detail__label">{i18n('icu:sent')}</td>
|
||||
<td>
|
||||
<ContextMenu
|
||||
i18n={i18n}
|
||||
menuOptions={[
|
||||
{
|
||||
icon: 'StoryDetailsModal__copy-icon',
|
||||
label: i18n('icu:StoryDetailsModal__copy-timestamp'),
|
||||
onClick: () => {
|
||||
void window.navigator.clipboard.writeText(String(sentAt));
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<>
|
||||
<Time timestamp={sentAt}>
|
||||
{formatDateTimeLong(i18n, sentAt)}
|
||||
</Time>{' '}
|
||||
<span className="module-message-detail__unix-timestamp">
|
||||
({receivedAt})
|
||||
({sentAt})
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{timeRemaining && timeRemaining > 0 && (
|
||||
<tr>
|
||||
<td className="module-message-detail__label">
|
||||
{i18n('icu:MessageDetail--disappears-in')}
|
||||
</td>
|
||||
<td>
|
||||
{formatRelativeTime(i18n, timeRemaining, {
|
||||
largest: 2,
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{this.renderContacts()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</>
|
||||
</ContextMenu>
|
||||
</td>
|
||||
</tr>
|
||||
{receivedAt && message.direction === 'incoming' ? (
|
||||
<tr>
|
||||
<td className="module-message-detail__label">
|
||||
{i18n('icu:received')}
|
||||
</td>
|
||||
<td>
|
||||
<Time timestamp={receivedAt}>
|
||||
{formatDateTimeLong(i18n, receivedAt)}
|
||||
</Time>{' '}
|
||||
<span className="module-message-detail__unix-timestamp">
|
||||
({receivedAt})
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{timeRemaining && timeRemaining > 0 && (
|
||||
<tr>
|
||||
<td className="module-message-detail__label">
|
||||
{i18n('icu:MessageDetail--disappears-in')}
|
||||
</td>
|
||||
<td>
|
||||
{formatRelativeTime(i18n, timeRemaining, {
|
||||
largest: 2,
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{renderContacts()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue