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

@ -1,9 +1,8 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { VirtualElement } from '@popperjs/core';
import React from 'react';
import { Manager, Popper, Reference } from 'react-popper';
import React, { useEffect, useState } from 'react';
import { usePopper } from 'react-popper';
import { createPortal } from 'react-dom';
import { showSettings } from '../shims/Whisper';
@ -38,240 +37,183 @@ export type PropsType = {
toggleStoriesView: () => unknown;
};
type StateType = {
showingAvatarPopup: boolean;
popperRoot: HTMLDivElement | null;
outsideClickDestructor?: () => void;
virtualElement: {
getBoundingClientRect: () => DOMRect;
};
};
export function MainHeader({
areStoriesEnabled,
avatarPath,
badge,
color,
hasFailedStorySends,
hasPendingUpdate,
i18n,
name,
phoneNumber,
profileName,
showArchivedConversations,
startComposing,
startUpdate,
theme,
title,
toggleProfileEditor,
toggleStoriesView,
unreadStoriesCount,
}: PropsType): JSX.Element {
const [targetElement, setTargetElement] = useState<HTMLElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
// https://popper.js.org/docs/v2/virtual-elements/
// Generating a virtual element here so that we can make the menu pop up
// right under the mouse cursor.
function generateVirtualElement(x: number, y: number): VirtualElement {
return {
getBoundingClientRect: () => new DOMRect(x, y),
};
}
const [showAvatarPopup, setShowAvatarPopup] = useState(false);
export class MainHeader extends React.Component<PropsType, StateType> {
public containerRef: React.RefObject<HTMLDivElement> = React.createRef();
const popper = usePopper(targetElement, popperElement, {
placement: 'bottom-start',
strategy: 'fixed',
modifiers: [
{
name: 'offset',
options: {
offset: [null, 4],
},
},
],
});
constructor(props: PropsType) {
super(props);
this.state = {
showingAvatarPopup: false,
popperRoot: null,
virtualElement: generateVirtualElement(0, 0),
useEffect(() => {
const div = document.createElement('div');
document.body.appendChild(div);
setPortalElement(div);
return () => {
div.remove();
setPortalElement(null);
};
}
}, []);
public showAvatarPopup = (ev: React.MouseEvent): void => {
const popperRoot = document.createElement('div');
document.body.appendChild(popperRoot);
const outsideClickDestructor = handleOutsideClick(
useEffect(() => {
return handleOutsideClick(
() => {
const { showingAvatarPopup } = this.state;
if (!showingAvatarPopup) {
if (!showAvatarPopup) {
return false;
}
this.hideAvatarPopup();
setShowAvatarPopup(false);
return true;
},
{
containerElements: [popperRoot, this.containerRef],
containerElements: [portalElement, targetElement],
name: 'MainHeader.showAvatarPopup',
}
);
}, [portalElement, targetElement, showAvatarPopup]);
this.setState({
showingAvatarPopup: true,
popperRoot,
outsideClickDestructor,
virtualElement: generateVirtualElement(ev.clientX, ev.clientY),
});
};
public hideAvatarPopup = (): void => {
const { popperRoot, outsideClickDestructor } = this.state;
this.setState({
showingAvatarPopup: false,
popperRoot: null,
outsideClickDestructor: undefined,
});
outsideClickDestructor?.();
if (popperRoot && document.body.contains(popperRoot)) {
document.body.removeChild(popperRoot);
useEffect(() => {
function handleGlobalKeyDown(event: KeyboardEvent) {
if (showAvatarPopup && event.key === 'Escape') {
setShowAvatarPopup(false);
}
}
};
document.addEventListener('keydown', handleGlobalKeyDown, true);
return () => {
document.removeEventListener('keydown', handleGlobalKeyDown, true);
};
}, [showAvatarPopup]);
public override componentDidMount(): void {
const useCapture = true;
document.addEventListener('keydown', this.handleGlobalKeyDown, useCapture);
}
public override componentWillUnmount(): void {
const { popperRoot, outsideClickDestructor } = this.state;
const useCapture = true;
outsideClickDestructor?.();
document.removeEventListener(
'keydown',
this.handleGlobalKeyDown,
useCapture
);
if (popperRoot && document.body.contains(popperRoot)) {
document.body.removeChild(popperRoot);
}
}
public handleGlobalKeyDown = (event: KeyboardEvent): void => {
const { showingAvatarPopup } = this.state;
const { key } = event;
if (showingAvatarPopup && key === 'Escape') {
this.hideAvatarPopup();
}
};
public override render(): JSX.Element {
const {
areStoriesEnabled,
avatarPath,
badge,
color,
hasFailedStorySends,
hasPendingUpdate,
i18n,
name,
phoneNumber,
profileName,
showArchivedConversations,
startComposing,
startUpdate,
theme,
title,
toggleProfileEditor,
toggleStoriesView,
unreadStoriesCount,
} = this.props;
const { showingAvatarPopup, popperRoot } = this.state;
return (
<div className="module-main-header">
<Manager>
<Reference>
{({ ref }) => (
<div
className="module-main-header__avatar--container"
ref={this.containerRef}
>
<Avatar
acceptedMessageRequest
avatarPath={avatarPath}
badge={badge}
className="module-main-header__avatar"
color={color}
conversationType="direct"
i18n={i18n}
isMe
phoneNumber={phoneNumber}
profileName={profileName}
theme={theme}
title={title}
// `sharedGroupNames` makes no sense for yourself, but
// `<Avatar>` needs it to determine blurring.
sharedGroupNames={[]}
size={AvatarSize.TWENTY_EIGHT}
innerRef={ref}
onClick={this.showAvatarPopup}
/>
{hasPendingUpdate && (
<div className="module-main-header__avatar--badged" />
)}
</div>
)}
</Reference>
{showingAvatarPopup && popperRoot
? createPortal(
<Popper referenceElement={this.state.virtualElement}>
{({ ref, style }) => (
<AvatarPopup
acceptedMessageRequest
badge={badge}
innerRef={ref}
i18n={i18n}
isMe
style={{ ...style, zIndex: 10 }}
color={color}
conversationType="direct"
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
theme={theme}
title={title}
avatarPath={avatarPath}
hasPendingUpdate={hasPendingUpdate}
startUpdate={startUpdate}
// See the comment above about `sharedGroupNames`.
sharedGroupNames={[]}
onEditProfile={() => {
toggleProfileEditor();
this.hideAvatarPopup();
}}
onViewPreferences={() => {
showSettings();
this.hideAvatarPopup();
}}
onViewArchive={() => {
showArchivedConversations();
this.hideAvatarPopup();
}}
/>
)}
</Popper>,
popperRoot
)
: null}
</Manager>
<div className="module-main-header__icon-container">
{areStoriesEnabled && (
<button
aria-label={i18n('icu:stories')}
className="module-main-header__stories-icon"
onClick={toggleStoriesView}
title={i18n('icu:stories')}
type="button"
>
{hasFailedStorySends && (
<span className="module-main-header__stories-badge">!</span>
)}
{!hasFailedStorySends && unreadStoriesCount ? (
<span className="module-main-header__stories-badge">
{unreadStoriesCount}
</span>
) : undefined}
</button>
)}
<button
aria-label={i18n('icu:newConversation')}
className="module-main-header__compose-icon"
onClick={startComposing}
title={i18n('icu:newConversation')}
type="button"
/>
</div>
return (
<div className="module-main-header">
<div
className="module-main-header__avatar--container"
ref={setTargetElement}
>
<Avatar
acceptedMessageRequest
avatarPath={avatarPath}
badge={badge}
className="module-main-header__avatar"
color={color}
conversationType="direct"
i18n={i18n}
isMe
phoneNumber={phoneNumber}
profileName={profileName}
theme={theme}
title={title}
// `sharedGroupNames` makes no sense for yourself, but
// `<Avatar>` needs it to determine blurring.
sharedGroupNames={[]}
size={AvatarSize.TWENTY_EIGHT}
onClick={() => {
setShowAvatarPopup(true);
}}
/>
{hasPendingUpdate && (
<div className="module-main-header__avatar--badged" />
)}
</div>
);
}
{showAvatarPopup &&
portalElement != null &&
createPortal(
<div
ref={setPopperElement}
style={{ ...popper.styles.popper, zIndex: 10 }}
{...popper.attributes.popper}
>
<AvatarPopup
acceptedMessageRequest
badge={badge}
i18n={i18n}
isMe
color={color}
conversationType="direct"
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
theme={theme}
title={title}
avatarPath={avatarPath}
hasPendingUpdate={hasPendingUpdate}
startUpdate={startUpdate}
// See the comment above about `sharedGroupNames`.
sharedGroupNames={[]}
onEditProfile={() => {
toggleProfileEditor();
setShowAvatarPopup(false);
}}
onViewPreferences={() => {
showSettings();
setShowAvatarPopup(false);
}}
onViewArchive={() => {
showArchivedConversations();
setShowAvatarPopup(false);
}}
style={{}}
/>
</div>,
portalElement
)}
<div className="module-main-header__icon-container">
{areStoriesEnabled && (
<button
aria-label={i18n('icu:stories')}
className="module-main-header__stories-icon"
onClick={toggleStoriesView}
title={i18n('icu:stories')}
type="button"
>
{hasFailedStorySends && (
<span className="module-main-header__stories-badge">!</span>
)}
{!hasFailedStorySends && unreadStoriesCount ? (
<span className="module-main-header__stories-badge">
{unreadStoriesCount}
</span>
) : undefined}
</button>
)}
<button
aria-label={i18n('icu:newConversation')}
className="module-main-header__compose-icon"
onClick={startComposing}
title={i18n('icu:newConversation')}
type="button"
/>
</div>
</div>
);
}