Update nav tab badges, fix several call tabs issues

This commit is contained in:
Jamie Kyle 2023-08-14 16:28:47 -07:00 committed by Jamie Kyle
parent ed6ffb695a
commit 9c7dc22a23
43 changed files with 1095 additions and 936 deletions

View file

@ -1,111 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { boolean, select, text } from '@storybook/addon-knobs';
import type { Props } from './AvatarPopup';
import { AvatarPopup } from './AvatarPopup';
import type { AvatarColorType } from '../types/Colors';
import { AvatarColors } from '../types/Colors';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
import { getFakeBadge } from '../test-both/helpers/getFakeBadge';
const i18n = setupI18n('en', enMessages);
const colorMap: Record<string, AvatarColorType> = AvatarColors.reduce(
(m, color) => ({
...m,
[color]: color,
}),
{}
);
const conversationTypeMap: Record<string, Props['conversationType']> = {
direct: 'direct',
group: 'group',
};
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
acceptedMessageRequest: true,
avatarPath: text('avatarPath', overrideProps.avatarPath || ''),
badge: overrideProps.badge,
color: select('color', colorMap, overrideProps.color || AvatarColors[0]),
conversationType: select(
'conversationType',
conversationTypeMap,
overrideProps.conversationType || 'direct'
),
hasPendingUpdate: Boolean(overrideProps.hasPendingUpdate),
i18n,
isMe: true,
noteToSelf: boolean('noteToSelf', overrideProps.noteToSelf || false),
onEditProfile: action('onEditProfile'),
onStartUpdate: action('startUpdate'),
phoneNumber: text('phoneNumber', overrideProps.phoneNumber || ''),
profileName: text('profileName', overrideProps.profileName || ''),
sharedGroupNames: [],
style: {},
theme: React.useContext(StorybookThemeContext),
title: text('title', overrideProps.title || ''),
});
export default {
title: 'Components/Avatar Popup',
};
export function AvatarOnly(): JSX.Element {
const props = useProps();
return <AvatarPopup {...props} />;
}
export function HasBadge(): JSX.Element {
const props = useProps({
badge: getFakeBadge(),
title: 'Janet Yellen',
});
return <AvatarPopup {...props} />;
}
HasBadge.story = {
name: 'Has badge',
};
export function Title(): JSX.Element {
const props = useProps({
title: 'My Great Title',
});
return <AvatarPopup {...props} />;
}
export function ProfileName(): JSX.Element {
const props = useProps({
profileName: 'Sam Neill',
});
return <AvatarPopup {...props} />;
}
export function PhoneNumber(): JSX.Element {
const props = useProps({
profileName: 'Sam Neill',
phoneNumber: '(555) 867-5309',
});
return <AvatarPopup {...props} />;
}
export function UpdateAvailable(): JSX.Element {
const props = useProps({
hasPendingUpdate: true,
});
return <AvatarPopup {...props} />;
}

View file

@ -1,93 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import classNames from 'classnames';
import type { Props as AvatarProps } from './Avatar';
import { Avatar, AvatarSize } from './Avatar';
import { useRestoreFocus } from '../hooks/useRestoreFocus';
import type { LocalizerType, ThemeType } from '../types/Util';
import { UserText } from './UserText';
export type Props = {
readonly i18n: LocalizerType;
readonly theme: ThemeType;
hasPendingUpdate: boolean;
onEditProfile: () => unknown;
onStartUpdate: () => unknown;
// Matches Popper's RefHandler type
innerRef?: React.Ref<HTMLDivElement>;
style: React.CSSProperties;
name?: string;
} & Omit<AvatarProps, 'onClick' | 'size'>;
export function AvatarPopup(props: Props): JSX.Element {
const {
hasPendingUpdate,
i18n,
name,
onEditProfile,
onStartUpdate,
phoneNumber,
profileName,
style,
title,
} = props;
const shouldShowNumber = Boolean(name || profileName);
// Note: mechanisms to dismiss this view are all in its host, MainHeader
// Focus first button after initial render, restore focus on teardown
const [focusRef] = useRestoreFocus();
return (
<div style={style} className="module-avatar-popup">
<button
className="module-avatar-popup__profile"
onClick={onEditProfile}
ref={focusRef}
type="button"
>
<Avatar {...props} size={AvatarSize.FORTY_EIGHT} />
<div className="module-avatar-popup__profile__text">
<div className="module-avatar-popup__profile__name">
<UserText text={profileName || title} />
</div>
{shouldShowNumber ? (
<div className="module-avatar-popup__profile__number">
{phoneNumber}
</div>
) : null}
</div>
</button>
{hasPendingUpdate && (
<>
<hr className="module-avatar-popup__divider" />
<button
type="button"
className="module-avatar-popup__item"
onClick={onStartUpdate}
>
<div
className={classNames(
'module-avatar-popup__item__icon',
'module-avatar-popup__item__icon--update'
)}
/>
<div className="module-avatar-popup__item__text">
{i18n('icu:avatarMenuUpdateAvailable')}
</div>
<div className="module-avatar-popup__item--badge" />
</button>
</>
)}
</div>
);
}

View file

@ -122,8 +122,10 @@ type CallsListProps = Readonly<{
) => void;
}>;
const CALL_LIST_ITEM_ROW_HEIGHT = 62;
function rowHeight() {
return ListTile.heightFull;
return CALL_LIST_ITEM_ROW_HEIGHT;
}
export function CallsList({
@ -275,6 +277,7 @@ export function CallsList({
return (
<div key={key} style={style}>
<ListTile
moduleClassName="CallsList__ItemTile"
leading={<div className="CallsList__LoadingAvatar" />}
title={
<span className="CallsList__LoadingText CallsList__LoadingText--title" />

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import type { LocalizerType } from '../types/I18N';
import { NavSidebar, NavSidebarActionButton } from './NavSidebar';
import { CallsList } from './CallsList';
@ -16,6 +16,7 @@ import { useEscapeHandling } from '../hooks/useEscapeHandling';
import type { ActiveCallStateType } from '../state/ducks/calling';
import { ContextMenu } from './ContextMenu';
import { ConfirmationDialog } from './ConfirmationDialog';
import type { UnreadStats } from '../util/countUnreadStats';
enum CallsTabSidebarView {
CallsListView,
@ -25,6 +26,7 @@ enum CallsTabSidebarView {
type CallsTabProps = Readonly<{
activeCall: ActiveCallStateType | undefined;
allConversations: ReadonlyArray<ConversationType>;
appUnreadStats: UnreadStats;
getCallHistoryGroupsCount: (
options: CallHistoryFilterOptions
) => Promise<number>;
@ -33,9 +35,12 @@ type CallsTabProps = Readonly<{
pagination: CallHistoryPagination
) => Promise<Array<CallHistoryGroup>>;
getConversation: (id: string) => ConversationType | void;
hasFailedStorySends: boolean;
hasPendingUpdate: boolean;
i18n: LocalizerType;
navTabsCollapsed: boolean;
onClearCallHistory: () => void;
onMarkCallHistoryRead: (conversationId: string, callId: string) => void;
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
@ -51,12 +56,16 @@ type CallsTabProps = Readonly<{
export function CallsTab({
activeCall,
allConversations,
appUnreadStats,
getCallHistoryGroupsCount,
getCallHistoryGroups,
getConversation,
hasFailedStorySends,
hasPendingUpdate,
i18n,
navTabsCollapsed,
onClearCallHistory,
onMarkCallHistoryRead,
onToggleNavTabsCollapse,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
@ -131,6 +140,14 @@ export function CallsTab({
[updateSidebarView, onOutgoingVideoCallInConversation]
);
useEffect(() => {
if (selected?.callHistoryGroup != null) {
selected.callHistoryGroup.children.forEach(child => {
onMarkCallHistoryRead(selected.conversationId, child.callId);
});
}
}, [selected, onMarkCallHistoryRead]);
return (
<>
<div className="CallsTab">
@ -141,6 +158,9 @@ export function CallsTab({
? i18n('icu:CallsTab__HeaderTitle--CallsList')
: i18n('icu:CallsTab__HeaderTitle--NewCall')
}
appUnreadStats={appUnreadStats}
hasFailedStorySends={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
navTabsCollapsed={navTabsCollapsed}
onBack={
sidebarView === CallsTabSidebarView.NewCallView
@ -232,7 +252,10 @@ export function CallsTab({
</NavSidebar>
{selected == null ? (
<div className="CallsTab__EmptyState">
{i18n('icu:CallsTab__EmptyStateText')}
<div className="CallsTab__EmptyStateIcon" />
<p className="CallsTab__EmptyStateLabel">
{i18n('icu:CallsTab__EmptyStateText')}
</p>
</div>
) : (
<div

View file

@ -6,9 +6,13 @@ import { Environment, getEnvironment } from '../environment';
import type { LocalizerType } from '../types/I18N';
import type { NavTabPanelProps } from './NavTabs';
import { WhatsNewLink } from './WhatsNewLink';
import type { UnreadStats } from '../util/countUnreadStats';
type ChatsTabProps = Readonly<{
appUnreadStats: UnreadStats;
i18n: LocalizerType;
hasPendingUpdate: boolean;
hasFailedStorySends: boolean;
navTabsCollapsed: boolean;
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
prevConversationId: string | undefined;
@ -20,7 +24,10 @@ type ChatsTabProps = Readonly<{
}>;
export function ChatsTab({
appUnreadStats,
i18n,
hasPendingUpdate,
hasFailedStorySends,
navTabsCollapsed,
onToggleNavTabsCollapse,
prevConversationId,
@ -34,7 +41,10 @@ export function ChatsTab({
<>
<div id="LeftPane">
{renderLeftPane({
appUnreadStats,
collapsed: navTabsCollapsed,
hasPendingUpdate,
hasFailedStorySends,
onToggleCollapse: onToggleNavTabsCollapse,
})}
</div>

View file

@ -133,6 +133,11 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
);
return {
appUnreadStats: {
unreadCount: 0,
unreadMentionsCount: 0,
markedUnread: false,
},
clearConversationSearch: action('clearConversationSearch'),
clearGroupCreationError: action('clearGroupCreationError'),
clearSearch: action('clearSearch'),
@ -143,6 +148,8 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
composeSaveAvatarToDisk: action('composeSaveAvatarToDisk'),
createGroup: action('createGroup'),
getPreferredBadge: () => undefined,
hasFailedStorySends: false,
hasPendingUpdate: false,
i18n,
isMacOS: boolean('isMacOS', false),
preferredWidthFromStorage: 320,

View file

@ -48,6 +48,7 @@ import {
NavSidebarSearchHeader,
} from './NavSidebar';
import { ContextMenu } from './ContextMenu';
import type { UnreadStats } from '../util/countUnreadStats';
export enum LeftPaneMode {
Inbox,
@ -59,8 +60,11 @@ export enum LeftPaneMode {
}
export type PropsType = {
appUnreadStats: UnreadStats;
hasExpiredDialog: boolean;
hasFailedStorySends: boolean;
hasNetworkDialog: boolean;
hasPendingUpdate: boolean;
hasRelinkDialog: boolean;
hasUpdateDialog: boolean;
isUpdateDownloaded: boolean;
@ -154,6 +158,7 @@ export type PropsType = {
} & LookupConversationWithoutServiceIdActionsType;
export function LeftPane({
appUnreadStats,
blockConversation,
challengeStatus,
clearConversationSearch,
@ -168,7 +173,9 @@ export function LeftPane({
createGroup,
getPreferredBadge,
hasExpiredDialog,
hasFailedStorySends,
hasNetworkDialog,
hasPendingUpdate,
hasRelinkDialog,
hasUpdateDialog,
i18n,
@ -549,6 +556,9 @@ export function LeftPane({
modeSpecificProps.mode === LeftPaneMode.SetGroupMetadata
}
i18n={i18n}
appUnreadStats={appUnreadStats}
hasFailedStorySends={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
navTabsCollapsed={navTabsCollapsed}
onToggleNavTabsCollapse={toggleNavTabsCollapse}
preferredLeftPaneWidth={preferredWidthFromStorage}

View file

@ -9,6 +9,7 @@ import {
StoryViewModeType,
} from '../types/Stories';
import type { LocalizerType } from '../types/Util';
import { ThemeType } from '../types/Util';
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
import { ConfirmationDialog } from './ConfirmationDialog';
import { ContextMenu } from './ContextMenu';
@ -19,9 +20,13 @@ import { Theme } from '../util/theme';
import { resolveStorySendStatus } from '../util/resolveStorySendStatus';
import { useRetryStorySend } from '../hooks/useRetryStorySend';
import { NavSidebar } from './NavSidebar';
import type { UnreadStats } from '../util/countUnreadStats';
export type PropsType = {
i18n: LocalizerType;
appUnreadStats: UnreadStats;
hasFailedStorySends: boolean;
hasPendingUpdate: boolean;
navTabsCollapsed: boolean;
myStories: Array<MyStoryType>;
onBack: () => unknown;
@ -36,10 +41,14 @@ export type PropsType = {
hasViewReceiptSetting: boolean;
preferredLeftPaneWidth: number;
savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void;
theme: ThemeType;
};
export function MyStories({
i18n,
appUnreadStats,
hasFailedStorySends,
hasPendingUpdate,
navTabsCollapsed,
myStories,
onBack,
@ -54,6 +63,7 @@ export function MyStories({
onToggleNavTabsCollapse,
preferredLeftPaneWidth,
savePreferredLeftPaneWidth,
theme,
}: PropsType): JSX.Element {
const [confirmDeleteStory, setConfirmDeleteStory] = useState<
StoryViewType | undefined
@ -80,6 +90,9 @@ export function MyStories({
<NavSidebar
i18n={i18n}
title={i18n('icu:MyStories__title')}
appUnreadStats={appUnreadStats}
hasFailedStorySends={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
navTabsCollapsed={navTabsCollapsed}
onBack={onBack}
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
@ -109,6 +122,7 @@ export function MyStories({
retryMessageSend={retryMessageSend}
setConfirmDeleteStory={setConfirmDeleteStory}
story={story}
theme={theme}
viewStory={viewStory}
/>
))}
@ -135,6 +149,7 @@ type StorySentPropsType = Pick<
| 'retryMessageSend'
| 'viewStory'
| 'onMediaPlaybackStart'
| 'theme'
> & {
setConfirmDeleteStory: (_: StoryViewType | undefined) => unknown;
story: StoryViewType;
@ -150,6 +165,7 @@ function StorySent({
retryMessageSend,
setConfirmDeleteStory,
story,
theme,
viewStory,
}: StorySentPropsType): JSX.Element {
const sendStatus = resolveStorySendStatus(story.sendState ?? []);
@ -278,7 +294,7 @@ function StorySent({
},
]}
moduleClassName="MyStories__story__more"
theme={Theme.Dark}
theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light}
/>
</div>
);

View file

@ -14,6 +14,7 @@ import {
getWidthFromPreferredWidth,
} from '../util/leftPaneWidth';
import { WidthBreakpoint, getNavSidebarWidthBreakpoint } from './_util';
import type { UnreadStats } from '../util/countUnreadStats';
export function NavSidebarActionButton({
icon,
@ -43,6 +44,8 @@ export type NavSidebarProps = Readonly<{
actions?: ReactNode;
children: ReactNode;
i18n: LocalizerType;
hasFailedStorySends: boolean;
hasPendingUpdate: boolean;
hideHeader?: boolean;
navTabsCollapsed: boolean;
onBack?: (() => void) | null;
@ -51,6 +54,7 @@ export type NavSidebarProps = Readonly<{
requiresFullWidth: boolean;
savePreferredLeftPaneWidth: (width: number) => void;
title: string;
appUnreadStats: UnreadStats;
}>;
enum DragState {
@ -64,6 +68,8 @@ export function NavSidebar({
children,
hideHeader,
i18n,
hasFailedStorySends,
hasPendingUpdate,
navTabsCollapsed,
onBack,
onToggleNavTabsCollapse,
@ -71,6 +77,7 @@ export function NavSidebar({
requiresFullWidth,
savePreferredLeftPaneWidth,
title,
appUnreadStats,
}: NavSidebarProps): JSX.Element {
const [dragState, setDragState] = useState(DragState.INITIAL);
@ -155,6 +162,9 @@ export function NavSidebar({
i18n={i18n}
navTabsCollapsed={navTabsCollapsed}
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
hasFailedStorySends={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
appUnreadStats={appUnreadStats}
/>
)}
<div

View file

@ -1,32 +1,91 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Key, ReactNode } from 'react';
import React, { useEffect, useState } from 'react';
import type { Key } from 'react';
import React from 'react';
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'react-aria-components';
import classNames from 'classnames';
import { usePopper } from 'react-popper';
import { createPortal } from 'react-dom';
import { Avatar, AvatarSize } from './Avatar';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import type { BadgeType } from '../badges/types';
import { AvatarPopup } from './AvatarPopup';
import { handleOutsideClick } from '../util/handleOutsideClick';
import type { UnreadStats } from '../state/selectors/conversations';
import { NavTab } from '../state/ducks/nav';
import { Tooltip, TooltipPlacement } from './Tooltip';
import { Theme } from '../util/theme';
import type { UnreadStats } from '../util/countUnreadStats';
import { ContextMenu } from './ContextMenu';
type NavTabsItemBadgesProps = Readonly<{
i18n: LocalizerType;
hasError?: boolean;
hasPendingUpdate?: boolean;
unreadStats: UnreadStats | null;
}>;
function NavTabsItemBadges({
i18n,
hasError,
hasPendingUpdate,
unreadStats,
}: NavTabsItemBadgesProps) {
if (hasError) {
return (
<span className="NavTabs__ItemUnreadBadge">
<span className="NavTabs__ItemIconLabel">
{i18n('icu:NavTabs__ItemIconLabel--HasError')}
</span>
<span aria-hidden>!</span>
</span>
);
}
if (hasPendingUpdate) {
return <div className="NavTabs__ItemUpdateBadge" />;
}
if (unreadStats != null) {
if (unreadStats.unreadCount > 0) {
return (
<span className="NavTabs__ItemUnreadBadge">
<span className="NavTabs__ItemIconLabel">
{i18n('icu:NavTabs__ItemIconLabel--UnreadCount', {
count: unreadStats.unreadCount,
})}
</span>
<span aria-hidden>{unreadStats.unreadCount}</span>
</span>
);
}
if (unreadStats.markedUnread) {
return (
<span className="NavTabs__ItemUnreadBadge">
{i18n('icu:NavTabs__ItemIconLabel--MarkedUnread')}
</span>
);
}
}
return null;
}
type NavTabProps = Readonly<{
i18n: LocalizerType;
badge?: ReactNode;
iconClassName: string;
id: NavTab;
hasError?: boolean;
label: string;
unreadStats: UnreadStats | null;
}>;
function NavTabsItem({ i18n, badge, iconClassName, id, label }: NavTabProps) {
function NavTabsItem({
i18n,
iconClassName,
id,
label,
unreadStats,
hasError,
}: NavTabProps) {
const isRTL = i18n.getLocaleDirection() === 'rtl';
return (
<Tab id={id} data-testid={`NavTabsItem--${id}`} className="NavTabs__Item">
@ -43,7 +102,11 @@ function NavTabsItem({ i18n, badge, iconClassName, id, label }: NavTabProps) {
role="presentation"
className={`NavTabs__ItemIcon ${iconClassName}`}
/>
{badge && <span className="NavTabs__ItemBadge">{badge}</span>}
<NavTabsItemBadges
i18n={i18n}
unreadStats={unreadStats}
hasError={hasError}
/>
</span>
</span>
</Tooltip>
@ -52,19 +115,28 @@ function NavTabsItem({ i18n, badge, iconClassName, id, label }: NavTabProps) {
}
export type NavTabPanelProps = Readonly<{
appUnreadStats: UnreadStats;
collapsed: boolean;
hasFailedStorySends: boolean;
hasPendingUpdate: boolean;
onToggleCollapse(collapsed: boolean): void;
}>;
export type NavTabsToggleProps = Readonly<{
appUnreadStats: UnreadStats | null;
i18n: LocalizerType;
hasFailedStorySends: boolean;
hasPendingUpdate: boolean;
navTabsCollapsed: boolean;
onToggleNavTabsCollapse(navTabsCollapsed: boolean): void;
}>;
export function NavTabsToggle({
i18n,
hasFailedStorySends,
hasPendingUpdate,
navTabsCollapsed,
appUnreadStats,
onToggleNavTabsCollapse,
}: NavTabsToggleProps): JSX.Element {
function handleToggle() {
@ -87,11 +159,19 @@ export function NavTabsToggle({
delay={600}
>
<span className="NavTabs__ItemButton">
<span
role="presentation"
className="NavTabs__ItemIcon NavTabs__ItemIcon--Menu"
/>
<span className="NavTabs__ItemLabel">{label}</span>
<span className="NavTabs__ItemContent">
<span
role="presentation"
className="NavTabs__ItemIcon NavTabs__ItemIcon--Menu"
/>
<span className="NavTabs__ItemLabel">{label}</span>
<NavTabsItemBadges
i18n={i18n}
unreadStats={appUnreadStats}
hasError={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
/>
</span>
</span>
</Tooltip>
</button>
@ -116,6 +196,7 @@ export type NavTabsProps = Readonly<{
selectedNavTab: NavTab;
storiesEnabled: boolean;
theme: ThemeType;
unreadCallsCount: number;
unreadConversationsStats: UnreadStats;
unreadStoriesCount: number;
}>;
@ -138,6 +219,7 @@ export function NavTabs({
selectedNavTab,
storiesEnabled,
theme,
unreadCallsCount,
unreadConversationsStats,
unreadStoriesCount,
}: NavTabsProps): JSX.Element {
@ -147,63 +229,6 @@ export function NavTabs({
const isRTL = i18n.getLocaleDirection() === 'rtl';
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);
};
}, []);
useEffect(() => {
return handleOutsideClick(
() => {
if (!showAvatarPopup) {
return false;
}
setShowAvatarPopup(false);
return true;
},
{
containerElements: [portalElement, targetElement],
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 (
<Tabs orientation="vertical" className="NavTabs__Container">
<nav
@ -215,6 +240,10 @@ export function NavTabs({
i18n={i18n}
navTabsCollapsed={navTabsCollapsed}
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
// These are all shown elsewhere when nav tabs are shown
hasFailedStorySends={false}
hasPendingUpdate={false}
appUnreadStats={null}
/>
<TabList
className="NavTabs__TabList"
@ -226,31 +255,18 @@ export function NavTabs({
id={NavTab.Chats}
label="Chats"
iconClassName="NavTabs__ItemIcon--Chats"
badge={
// eslint-disable-next-line no-nested-ternary
unreadConversationsStats.unreadCount > 0 ? (
<>
<span className="NavTabs__ItemIconLabel">
{i18n('icu:NavTabs__ItemIconLabel--UnreadCount', {
count: unreadConversationsStats.unreadCount,
})}
</span>
<span aria-hidden>
{unreadConversationsStats.unreadCount}
</span>
</>
) : unreadConversationsStats.markedUnread ? (
<span className="NavTabs__ItemIconLabel">
{i18n('icu:NavTabs__ItemIconLabel--MarkedUnread')}
</span>
) : null
}
unreadStats={unreadConversationsStats}
/>
<NavTabsItem
i18n={i18n}
id={NavTab.Calls}
label="Calls"
iconClassName="NavTabs__ItemIcon--Calls"
unreadStats={{
unreadCount: unreadCallsCount,
unreadMentionsCount: 0,
markedUnread: false,
}}
/>
{storiesEnabled && (
<NavTabsItem
@ -258,47 +274,89 @@ export function NavTabs({
id={NavTab.Stories}
label="Stories"
iconClassName="NavTabs__ItemIcon--Stories"
badge={
// eslint-disable-next-line no-nested-ternary
hasFailedStorySends
? '!'
: unreadStoriesCount > 0
? unreadStoriesCount
: null
}
hasError={hasFailedStorySends}
unreadStats={{
unreadCount: unreadStoriesCount,
unreadMentionsCount: 0,
markedUnread: false,
}}
/>
)}
</TabList>
<div className="NavTabs__Misc">
<button
type="button"
className="NavTabs__Item"
onClick={onShowSettings}
<ContextMenu
i18n={i18n}
menuOptions={[
{
icon: 'NavTabs__ContextMenuIcon--Settings',
label: i18n('icu:NavTabs__ItemLabel--Settings'),
onClick: onShowSettings,
},
{
icon: 'NavTabs__ContextMenuIcon--Update',
label: i18n('icu:NavTabs__ItemLabel--Update'),
onClick: onStartUpdate,
},
]}
popperOptions={{
placement: 'top-start',
strategy: 'absolute',
}}
portalToRoot
>
<Tooltip
content={i18n('icu:NavTabs__ItemLabel--Settings')}
theme={Theme.Dark}
direction={TooltipPlacement.Right}
delay={600}
>
<span className="NavTabs__ItemButton">
<span
role="presentation"
className="NavTabs__ItemIcon NavTabs__ItemIcon--Settings"
/>
<span className="NavTabs__ItemLabel">
{i18n('icu:NavTabs__ItemLabel--Settings')}
</span>
</span>
</Tooltip>
</button>
{({ openMenu, onKeyDown, ref }) => {
return (
<button
type="button"
className="NavTabs__Item"
onKeyDown={event => {
if (hasPendingUpdate) {
onKeyDown(event);
}
}}
onClick={event => {
if (hasPendingUpdate) {
openMenu(event);
} else {
onShowSettings();
}
}}
>
<Tooltip
content={i18n('icu:NavTabs__ItemLabel--Settings')}
theme={Theme.Dark}
direction={TooltipPlacement.Right}
delay={600}
>
<span className="NavTabs__ItemButton" ref={ref}>
<span className="NavTabs__ItemContent">
<span
role="presentation"
className="NavTabs__ItemIcon NavTabs__ItemIcon--Settings"
/>
<span className="NavTabs__ItemLabel">
{i18n('icu:NavTabs__ItemLabel--Settings')}
</span>
<NavTabsItemBadges
i18n={i18n}
unreadStats={null}
hasPendingUpdate={hasPendingUpdate}
/>
</span>
</span>
</Tooltip>
</button>
);
}}
</ContextMenu>
<button
type="button"
className="NavTabs__Item NavTabs__Item--Profile"
data-supertab
onClick={() => {
setShowAvatarPopup(true);
onToggleProfileEditor();
}}
aria-label={i18n('icu:NavTabs__ItemLabel--Profile')}
>
@ -308,7 +366,7 @@ export function NavTabs({
direction={isRTL ? TooltipPlacement.Left : TooltipPlacement.Right}
delay={600}
>
<span className="NavTabs__ItemButton" ref={setTargetElement}>
<span className="NavTabs__ItemButton">
<span className="NavTabs__ItemContent">
<Avatar
acceptedMessageRequest
@ -328,49 +386,10 @@ export function NavTabs({
sharedGroupNames={[]}
size={AvatarSize.TWENTY_EIGHT}
/>
{hasPendingUpdate && <div className="NavTabs__AvatarBadge" />}
</span>
</span>
</Tooltip>
</button>
{showAvatarPopup &&
portalElement != null &&
createPortal(
<div
id="MainHeader__AvatarPopup"
ref={setPopperElement}
style={{ ...popper.styles.popper, zIndex: 10 }}
{...popper.attributes.popper}
>
<AvatarPopup
acceptedMessageRequest
badge={badge}
i18n={i18n}
isMe
color={me.color}
conversationType="direct"
name={me.name}
phoneNumber={me.phoneNumber}
profileName={me.profileName}
theme={theme}
title={me.title}
avatarPath={me.avatarPath}
hasPendingUpdate={hasPendingUpdate}
// See the comment above about `sharedGroupNames`.
sharedGroupNames={[]}
onEditProfile={() => {
onToggleProfileEditor();
setShowAvatarPopup(false);
}}
onStartUpdate={() => {
onStartUpdate();
setShowAvatarPopup(false);
}}
style={{}}
/>
</div>,
portalElement
)}
</div>
</nav>
<TabPanels>

View file

@ -79,6 +79,7 @@ export type PropsDataType = {
hasCompletedUsernameLinkOnboarding: boolean;
i18n: LocalizerType;
isUsernameFlagEnabled: boolean;
phoneNumber?: string;
userAvatarData: ReadonlyArray<AvatarDataType>;
username?: string;
usernameEditState: UsernameEditState;
@ -154,6 +155,7 @@ export function ProfileEditor({
onProfileChanged,
onSetSkinTone,
openUsernameReservationModal,
phoneNumber,
profileAvatarPath,
recentEmojis,
renderEditUsernameModalBody,
@ -678,6 +680,10 @@ export function ProfileEditor({
width: 80,
}}
/>
<h1 className="ProfileEditor__Title">{getFullNameText()}</h1>
{phoneNumber != null && (
<p className="ProfileEditor__PhoneNumber">{phoneNumber}</p>
)}
<hr className="ProfileEditor__divider" />
<PanelRow
className="ProfileEditor__row"

View file

@ -24,11 +24,15 @@ import { StoriesPane } from './StoriesPane';
import { NavSidebar, NavSidebarActionButton } from './NavSidebar';
import { StoriesAddStoryButton } from './StoriesAddStoryButton';
import { ContextMenu } from './ContextMenu';
import type { UnreadStats } from '../util/countUnreadStats';
export type PropsType = {
addStoryData: AddStoryData;
appUnreadStats: UnreadStats;
deleteStoryForEveryone: (story: StoryViewType) => unknown;
getPreferredBadge: PreferredBadgeSelectorType;
hasFailedStorySends: boolean;
hasPendingUpdate: boolean;
hasViewReceiptSetting: boolean;
hiddenStories: Array<ConversationStoryType>;
i18n: LocalizerType;
@ -61,8 +65,11 @@ export type PropsType = {
export function StoriesTab({
addStoryData,
appUnreadStats,
deleteStoryForEveryone,
getPreferredBadge,
hasFailedStorySends,
hasPendingUpdate,
hasViewReceiptSetting,
hiddenStories,
i18n,
@ -104,6 +111,9 @@ export function StoriesTab({
{addStoryData && renderStoryCreator()}
{isMyStories && myStories.length ? (
<MyStories
appUnreadStats={appUnreadStats}
hasFailedStorySends={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
hasViewReceiptSetting={hasViewReceiptSetting}
i18n={i18n}
myStories={myStories}
@ -118,17 +128,21 @@ export function StoriesTab({
queueStoryDownload={queueStoryDownload}
retryMessageSend={retryMessageSend}
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
theme={theme}
viewStory={viewStory}
/>
) : (
<NavSidebar
title="Stories"
i18n={i18n}
hasFailedStorySends={hasFailedStorySends}
hasPendingUpdate={hasPendingUpdate}
navTabsCollapsed={navTabsCollapsed}
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
preferredLeftPaneWidth={preferredLeftPaneWidth}
requiresFullWidth
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
appUnreadStats={appUnreadStats}
actions={
<>
<StoriesAddStoryButton