Update nav tab badges, fix several call tabs issues
This commit is contained in:
parent
ed6ffb695a
commit
9c7dc22a23
43 changed files with 1095 additions and 936 deletions
|
@ -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} />;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue