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
|
|
|
|
|
2018-09-29 00:42:13 +00:00
|
|
|
import React from 'react';
|
2019-10-17 18:22:07 +00:00
|
|
|
import { Manager, Popper, Reference } from 'react-popper';
|
|
|
|
import { createPortal } from 'react-dom';
|
2018-09-29 00:42:13 +00:00
|
|
|
|
2019-10-17 18:22:07 +00:00
|
|
|
import { showSettings } from '../shims/Whisper';
|
2018-09-29 00:42:13 +00:00
|
|
|
import { Avatar } from './Avatar';
|
2019-10-17 18:22:07 +00:00
|
|
|
import { AvatarPopup } from './AvatarPopup';
|
2021-11-08 16:29:54 +00:00
|
|
|
import type { LocalizerType, ThemeType } from '../types/Util';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { AvatarColorType } from '../types/Colors';
|
2021-11-08 16:29:54 +00:00
|
|
|
import type { BadgeType } from '../badges/types';
|
2022-09-15 01:58:35 +00:00
|
|
|
import { handleOutsideClick } from '../util/handleOutsideClick';
|
2018-09-29 00:42:13 +00:00
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
export type PropsType = {
|
2022-03-04 21:14:52 +00:00
|
|
|
areStoriesEnabled: boolean;
|
2018-09-29 00:42:13 +00:00
|
|
|
avatarPath?: string;
|
2021-11-08 16:29:54 +00:00
|
|
|
badge?: BadgeType;
|
2022-01-27 22:12:26 +00:00
|
|
|
color?: AvatarColorType;
|
2021-08-19 22:56:29 +00:00
|
|
|
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;
|
2022-07-20 23:06:15 +00:00
|
|
|
unreadStoriesCount: number;
|
2019-10-17 18:22:07 +00:00
|
|
|
|
|
|
|
showArchivedConversations: () => void;
|
2021-02-23 20:34:28 +00:00
|
|
|
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;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2019-10-17 18:22:07 +00:00
|
|
|
|
2021-01-14 18:07:05 +00:00
|
|
|
type StateType = {
|
2019-10-17 18:22:07 +00:00
|
|
|
showingAvatarPopup: boolean;
|
|
|
|
popperRoot: HTMLDivElement | null;
|
2022-09-15 01:58:35 +00:00
|
|
|
outsideClickDestructor?: () => void;
|
2021-01-14 18:07:05 +00:00
|
|
|
};
|
2018-09-29 00:42:13 +00:00
|
|
|
|
2019-10-17 18:22:07 +00:00
|
|
|
export class MainHeader extends React.Component<PropsType, StateType> {
|
2022-06-15 17:53:08 +00:00
|
|
|
public containerRef: React.RefObject<HTMLDivElement> = React.createRef();
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-06-15 17:53:08 +00:00
|
|
|
public showAvatarPopup = (): void => {
|
2019-12-03 20:02:50 +00:00
|
|
|
const popperRoot = document.createElement('div');
|
|
|
|
document.body.appendChild(popperRoot);
|
|
|
|
|
2022-09-15 01:58:35 +00:00
|
|
|
const outsideClickDestructor = handleOutsideClick(
|
|
|
|
() => {
|
|
|
|
const { showingAvatarPopup } = this.state;
|
|
|
|
if (!showingAvatarPopup) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.hideAvatarPopup();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
},
|
2022-09-27 20:24:21 +00:00
|
|
|
{
|
|
|
|
containerElements: [popperRoot, this.containerRef],
|
|
|
|
name: 'MainHeader.showAvatarPopup',
|
|
|
|
}
|
2022-09-15 01:58:35 +00:00
|
|
|
);
|
|
|
|
|
2019-10-17 18:22:07 +00:00
|
|
|
this.setState({
|
|
|
|
showingAvatarPopup: true,
|
2019-12-03 20:02:50 +00:00
|
|
|
popperRoot,
|
2022-09-15 01:58:35 +00:00
|
|
|
outsideClickDestructor,
|
2019-10-17 18:22:07 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-09-12 00:46:52 +00:00
|
|
|
public hideAvatarPopup = (): void => {
|
2022-09-15 01:58:35 +00:00
|
|
|
const { popperRoot, outsideClickDestructor } = this.state;
|
2019-12-03 20:02:50 +00:00
|
|
|
|
2019-10-17 18:22:07 +00:00
|
|
|
this.setState({
|
|
|
|
showingAvatarPopup: false,
|
2019-12-03 20:02:50 +00:00
|
|
|
popperRoot: null,
|
2022-09-15 01:58:35 +00:00
|
|
|
outsideClickDestructor: undefined,
|
2019-10-17 18:22:07 +00:00
|
|
|
});
|
2019-12-03 20:02:50 +00:00
|
|
|
|
2022-09-15 01:58:35 +00:00
|
|
|
outsideClickDestructor?.();
|
|
|
|
|
2019-12-04 22:30:36 +00:00
|
|
|
if (popperRoot && document.body.contains(popperRoot)) {
|
2019-12-03 20:02:50 +00:00
|
|
|
document.body.removeChild(popperRoot);
|
|
|
|
}
|
2019-10-17 18:22:07 +00:00
|
|
|
};
|
|
|
|
|
2021-11-12 23:44:20 +00:00
|
|
|
public override componentDidMount(): void {
|
2022-09-15 01:58:35 +00:00
|
|
|
const useCapture = true;
|
|
|
|
document.addEventListener('keydown', this.handleGlobalKeyDown, useCapture);
|
2021-02-12 21:58:14 +00:00
|
|
|
}
|
|
|
|
|
2021-11-12 23:44:20 +00:00
|
|
|
public override componentWillUnmount(): void {
|
2022-09-15 01:58:35 +00:00
|
|
|
const { popperRoot, outsideClickDestructor } = this.state;
|
|
|
|
|
|
|
|
const useCapture = true;
|
|
|
|
outsideClickDestructor?.();
|
|
|
|
document.removeEventListener(
|
|
|
|
'keydown',
|
|
|
|
this.handleGlobalKeyDown,
|
|
|
|
useCapture
|
|
|
|
);
|
2019-12-03 20:02:50 +00:00
|
|
|
|
2019-12-04 22:30:36 +00:00
|
|
|
if (popperRoot && document.body.contains(popperRoot)) {
|
2019-10-17 18:22:07 +00:00
|
|
|
document.body.removeChild(popperRoot);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-12 21:58:14 +00:00
|
|
|
public handleGlobalKeyDown = (event: KeyboardEvent): void => {
|
|
|
|
const { showingAvatarPopup } = this.state;
|
2021-11-01 18:43:02 +00:00
|
|
|
const { key } = event;
|
2021-02-12 21:58:14 +00:00
|
|
|
|
|
|
|
if (showingAvatarPopup && key === 'Escape') {
|
|
|
|
this.hideAvatarPopup();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-11-12 23:44:20 +00:00
|
|
|
public override render(): JSX.Element {
|
2018-09-29 00:42:13 +00:00
|
|
|
const {
|
2022-03-04 21:14:52 +00:00
|
|
|
areStoriesEnabled,
|
2018-09-29 00:42:13 +00:00
|
|
|
avatarPath,
|
2021-11-08 16:29:54 +00:00
|
|
|
badge,
|
2018-09-29 00:42:13 +00:00
|
|
|
color,
|
2021-08-19 22:56:29 +00:00
|
|
|
hasPendingUpdate,
|
2019-08-09 23:12:29 +00:00
|
|
|
i18n,
|
2018-09-29 00:42:13 +00:00
|
|
|
name,
|
|
|
|
phoneNumber,
|
|
|
|
profileName,
|
2019-10-17 18:22:07 +00:00
|
|
|
showArchivedConversations,
|
2021-08-19 22:56:29 +00:00
|
|
|
startComposing,
|
|
|
|
startUpdate,
|
2021-11-08 16:29:54 +00:00
|
|
|
theme,
|
2021-08-19 22:56:29 +00:00
|
|
|
title,
|
2021-07-19 19:26:06 +00:00
|
|
|
toggleProfileEditor,
|
2022-03-04 21:14:52 +00:00
|
|
|
toggleStoriesView,
|
2022-07-20 23:06:15 +00:00
|
|
|
unreadStoriesCount,
|
2018-09-29 00:42:13 +00:00
|
|
|
} = this.props;
|
2019-10-17 18:22:07 +00:00
|
|
|
const { showingAvatarPopup, popperRoot } = this.state;
|
2018-09-29 00:42:13 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="module-main-header">
|
2019-10-17 18:22:07 +00:00
|
|
|
<Manager>
|
|
|
|
<Reference>
|
|
|
|
{({ ref }) => (
|
2022-06-15 17:53:08 +00:00
|
|
|
<div
|
|
|
|
className="module-main-header__avatar--container"
|
|
|
|
ref={this.containerRef}
|
|
|
|
>
|
2021-08-19 22:56:29 +00:00
|
|
|
<Avatar
|
|
|
|
acceptedMessageRequest
|
|
|
|
avatarPath={avatarPath}
|
2021-11-08 16:29:54 +00:00
|
|
|
badge={badge}
|
2021-08-19 22:56:29 +00:00
|
|
|
className="module-main-header__avatar"
|
|
|
|
color={color}
|
|
|
|
conversationType="direct"
|
|
|
|
i18n={i18n}
|
|
|
|
isMe
|
|
|
|
phoneNumber={phoneNumber}
|
|
|
|
profileName={profileName}
|
2021-11-08 16:29:54 +00:00
|
|
|
theme={theme}
|
2021-08-19 22:56:29 +00:00
|
|
|
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
|
2021-11-08 16:29:54 +00:00
|
|
|
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}
|
2021-11-08 16:29:54 +00:00
|
|
|
theme={theme}
|
2020-07-24 01:35:32 +00:00
|
|
|
title={title}
|
2019-10-17 18:22:07 +00:00
|
|
|
avatarPath={avatarPath}
|
|
|
|
size={28}
|
2021-08-19 22:56:29 +00:00
|
|
|
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"
|
2022-07-20 23:06:15 +00:00
|
|
|
>
|
|
|
|
{unreadStoriesCount ? (
|
|
|
|
<span className="module-main-header__stories-badge">
|
|
|
|
{unreadStoriesCount}
|
|
|
|
</span>
|
|
|
|
) : undefined}
|
|
|
|
</button>
|
2022-03-04 21:14:52 +00:00
|
|
|
)}
|
|
|
|
<button
|
|
|
|
aria-label={i18n('newConversation')}
|
|
|
|
className="module-main-header__compose-icon"
|
|
|
|
onClick={startComposing}
|
|
|
|
title={i18n('newConversation')}
|
|
|
|
type="button"
|
|
|
|
/>
|
|
|
|
</div>
|
2018-09-29 00:42:13 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|