signal-desktop/ts/components/MainHeader.tsx

220 lines
6.1 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2018 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useState } from 'react';
import { usePopper } from 'react-popper';
2019-10-17 18:22:07 +00:00
import { createPortal } from 'react-dom';
2019-10-17 18:22:07 +00:00
import { showSettings } from '../shims/Whisper';
2022-12-09 20:37:45 +00:00
import { Avatar, AvatarSize } 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';
import { handleOutsideClick } from '../util/handleOutsideClick';
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;
2023-02-07 19:33:04 +00:00
hasFailedStorySends?: boolean;
2022-07-20 23:06:15 +00:00
unreadStoriesCount: number;
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
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);
const [showAvatarPopup, setShowAvatarPopup] = useState(false);
const popper = usePopper(targetElement, popperElement, {
placement: 'bottom-start',
strategy: 'fixed',
modifiers: [
{
name: 'offset',
options: {
offset: [null, 4],
},
},
],
});
useEffect(() => {
const div = document.createElement('div');
document.body.appendChild(div);
setPortalElement(div);
return () => {
div.remove();
setPortalElement(null);
2019-10-17 18:22:07 +00:00
};
}, []);
2019-10-17 18:22:07 +00:00
useEffect(() => {
return handleOutsideClick(
() => {
if (!showAvatarPopup) {
return false;
}
setShowAvatarPopup(false);
return true;
},
2022-09-27 20:24:21 +00:00
{
containerElements: [portalElement, targetElement],
2022-09-27 20:24:21 +00:00
name: 'MainHeader.showAvatarPopup',
}
);
}, [portalElement, targetElement, showAvatarPopup]);
useEffect(() => {
function handleGlobalKeyDown(event: KeyboardEvent) {
if (showAvatarPopup && event.key === 'Escape') {
setShowAvatarPopup(false);
}
}
document.addEventListener('keydown', handleGlobalKeyDown, true);
return () => {
document.removeEventListener('keydown', handleGlobalKeyDown, true);
};
}, [showAvatarPopup]);
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 && (
2022-03-04 21:14:52 +00:00
<button
aria-label={i18n('icu:stories')}
className="module-main-header__stories-icon"
onClick={toggleStoriesView}
title={i18n('icu:stories')}
2022-03-04 21:14:52 +00:00
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>
);
}