signal-desktop/ts/components/MainHeader.tsx

233 lines
6.7 KiB
TypeScript
Raw Normal View History

2022-01-27 22:12:26 +00:00
// Copyright 2018-2022 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
2019-10-17 18:22:07 +00:00
import { Manager, Popper, Reference } from 'react-popper';
import { createPortal } from 'react-dom';
2019-10-17 18:22:07 +00:00
import { showSettings } from '../shims/Whisper';
import { Avatar } from './Avatar';
2019-10-17 18:22:07 +00:00
import { AvatarPopup } from './AvatarPopup';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { AvatarColorType } from '../types/Colors';
import type { BadgeType } from '../badges/types';
export type PropsType = {
2022-03-04 21:14:52 +00:00
areStoriesEnabled: boolean;
avatarPath?: string;
badge?: BadgeType;
2022-01-27 22:12:26 +00:00
color?: AvatarColorType;
hasPendingUpdate: boolean;
2019-01-14 21:49:58 +00:00
i18n: LocalizerType;
2022-01-27 22:12:26 +00:00
isMe?: boolean;
isVerified?: boolean;
name?: string;
phoneNumber?: string;
profileName?: string;
theme: ThemeType;
title: string;
2019-10-17 18:22:07 +00:00
showArchivedConversations: () => void;
startComposing: () => void;
2022-01-27 22:12:26 +00:00
startUpdate: () => unknown;
2021-07-19 19:26:06 +00:00
toggleProfileEditor: () => void;
2022-03-04 21:14:52 +00:00
toggleStoriesView: () => unknown;
};
2019-10-17 18:22:07 +00:00
type StateType = {
2019-10-17 18:22:07 +00:00
showingAvatarPopup: boolean;
popperRoot: HTMLDivElement | null;
};
2019-10-17 18:22:07 +00:00
export class MainHeader extends React.Component<PropsType, StateType> {
2019-08-09 23:12:29 +00:00
constructor(props: PropsType) {
2019-01-14 21:49:58 +00:00
super(props);
2019-10-17 18:22:07 +00:00
this.state = {
showingAvatarPopup: false,
popperRoot: null,
};
}
2020-09-12 00:46:52 +00:00
public handleOutsideClick = ({ target }: MouseEvent): void => {
2019-10-17 18:22:07 +00:00
const { popperRoot, showingAvatarPopup } = this.state;
if (
showingAvatarPopup &&
popperRoot &&
!popperRoot.contains(target as Node)
) {
this.hideAvatarPopup();
}
};
2020-09-12 00:46:52 +00:00
public showAvatarPopup = (): void => {
const popperRoot = document.createElement('div');
document.body.appendChild(popperRoot);
2019-10-17 18:22:07 +00:00
this.setState({
showingAvatarPopup: true,
popperRoot,
2019-10-17 18:22:07 +00:00
});
document.addEventListener('click', this.handleOutsideClick);
};
2020-09-12 00:46:52 +00:00
public hideAvatarPopup = (): void => {
const { popperRoot } = this.state;
2019-10-17 18:22:07 +00:00
document.removeEventListener('click', this.handleOutsideClick);
2019-10-17 18:22:07 +00:00
this.setState({
showingAvatarPopup: false,
popperRoot: null,
2019-10-17 18:22:07 +00:00
});
if (popperRoot && document.body.contains(popperRoot)) {
document.body.removeChild(popperRoot);
}
2019-10-17 18:22:07 +00:00
};
public override componentDidMount(): void {
document.addEventListener('keydown', this.handleGlobalKeyDown);
}
public override componentWillUnmount(): void {
2019-10-17 18:22:07 +00:00
const { popperRoot } = this.state;
document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('keydown', this.handleGlobalKeyDown);
if (popperRoot && document.body.contains(popperRoot)) {
2019-10-17 18:22:07 +00:00
document.body.removeChild(popperRoot);
}
}
public handleGlobalKeyDown = (event: KeyboardEvent): void => {
const { showingAvatarPopup } = this.state;
2021-11-01 18:43:02 +00:00
const { key } = event;
if (showingAvatarPopup && key === 'Escape') {
this.hideAvatarPopup();
}
};
public override render(): JSX.Element {
const {
2022-03-04 21:14:52 +00:00
areStoriesEnabled,
avatarPath,
badge,
color,
hasPendingUpdate,
2019-08-09 23:12:29 +00:00
i18n,
name,
phoneNumber,
profileName,
2019-10-17 18:22:07 +00:00
showArchivedConversations,
startComposing,
startUpdate,
theme,
title,
2021-07-19 19:26:06 +00:00
toggleProfileEditor,
2022-03-04 21:14:52 +00:00
toggleStoriesView,
} = this.props;
2019-10-17 18:22:07 +00:00
const { showingAvatarPopup, popperRoot } = this.state;
return (
<div className="module-main-header">
2019-10-17 18:22:07 +00:00
<Manager>
<Reference>
{({ ref }) => (
<div className="module-main-header__avatar--container">
<Avatar
acceptedMessageRequest
avatarPath={avatarPath}
badge={badge}
className="module-main-header__avatar"
color={color}
conversationType="direct"
i18n={i18n}
isMe
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
theme={theme}
title={title}
// `sharedGroupNames` makes no sense for yourself, but
// `<Avatar>` needs it to determine blurring.
sharedGroupNames={[]}
size={28}
innerRef={ref}
onClick={this.showAvatarPopup}
/>
{hasPendingUpdate && (
<div className="module-main-header__avatar--badged" />
)}
</div>
2019-10-17 18:22:07 +00:00
)}
</Reference>
{showingAvatarPopup && popperRoot
? createPortal(
<Popper placement="bottom-end">
{({ ref, style }) => (
<AvatarPopup
2021-05-07 22:21:10 +00:00
acceptedMessageRequest
badge={badge}
2019-10-17 18:22:07 +00:00
innerRef={ref}
i18n={i18n}
2021-05-07 22:21:10 +00:00
isMe
2021-10-02 00:01:44 +00:00
style={{ ...style, zIndex: 10 }}
2019-10-17 18:22:07 +00:00
color={color}
conversationType="direct"
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
theme={theme}
2020-07-24 01:35:32 +00:00
title={title}
2019-10-17 18:22:07 +00:00
avatarPath={avatarPath}
size={28}
hasPendingUpdate={hasPendingUpdate}
startUpdate={startUpdate}
2021-05-07 22:21:10 +00:00
// See the comment above about `sharedGroupNames`.
sharedGroupNames={[]}
2021-07-19 19:26:06 +00:00
onEditProfile={() => {
toggleProfileEditor();
this.hideAvatarPopup();
}}
2019-10-17 18:22:07 +00:00
onViewPreferences={() => {
showSettings();
this.hideAvatarPopup();
}}
onViewArchive={() => {
showArchivedConversations();
this.hideAvatarPopup();
}}
/>
)}
</Popper>,
popperRoot
)
: null}
</Manager>
2022-03-04 21:14:52 +00:00
<div className="module-main-header__icon-container">
{areStoriesEnabled && (
<button
aria-label={i18n('stories')}
className="module-main-header__stories-icon"
onClick={toggleStoriesView}
title={i18n('stories')}
type="button"
/>
)}
<button
aria-label={i18n('newConversation')}
className="module-main-header__compose-icon"
onClick={startComposing}
title={i18n('newConversation')}
type="button"
/>
</div>
</div>
);
}
}