Migrate most React class components to function components

This commit is contained in:
Jamie Kyle 2023-04-12 16:17:56 -07:00 committed by GitHub
parent 4c9baaef80
commit 558b5a4a38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1444 additions and 1775 deletions

View file

@ -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>
);
}