Update call tab design based on feedback
This commit is contained in:
parent
ce28993c78
commit
3268d3e6eb
32 changed files with 601 additions and 289 deletions
|
@ -38,6 +38,7 @@ export enum AvatarSize {
|
|||
TWENTY = 20,
|
||||
TWENTY_EIGHT = 28,
|
||||
THIRTY_TWO = 32,
|
||||
THIRTY_SIX = 36,
|
||||
FORTY_EIGHT = 48,
|
||||
FIFTY_TWO = 52,
|
||||
EIGHTY = 80,
|
||||
|
|
|
@ -40,6 +40,9 @@ import { Intl } from './Intl';
|
|||
import { NavSidebarSearchHeader } from './NavSidebar';
|
||||
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||
import { formatCallHistoryGroup } from '../util/callDisposition';
|
||||
import { CallsNewCallButton } from './CallsNewCall';
|
||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
import { Theme } from '../util/theme';
|
||||
|
||||
function Timestamp({
|
||||
i18n,
|
||||
|
@ -100,6 +103,7 @@ const defaultPendingState: SearchState = {
|
|||
};
|
||||
|
||||
type CallsListProps = Readonly<{
|
||||
hasActiveCall: boolean;
|
||||
getCallHistoryGroupsCount: (
|
||||
options: CallHistoryFilterOptions
|
||||
) => Promise<number>;
|
||||
|
@ -110,6 +114,8 @@ type CallsListProps = Readonly<{
|
|||
getConversation: (id: string) => ConversationType | void;
|
||||
i18n: LocalizerType;
|
||||
selectedCallHistoryGroup: CallHistoryGroup | null;
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||
onSelectCallHistoryGroup: (
|
||||
conversationId: string,
|
||||
selectedCallHistoryGroup: CallHistoryGroup
|
||||
|
@ -117,15 +123,18 @@ type CallsListProps = Readonly<{
|
|||
}>;
|
||||
|
||||
function rowHeight() {
|
||||
return ListTile.heightCompact;
|
||||
return ListTile.heightFull;
|
||||
}
|
||||
|
||||
export function CallsList({
|
||||
hasActiveCall,
|
||||
getCallHistoryGroupsCount,
|
||||
getCallHistoryGroups,
|
||||
getConversation,
|
||||
i18n,
|
||||
selectedCallHistoryGroup,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
onSelectCallHistoryGroup,
|
||||
}: CallsListProps): JSX.Element {
|
||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
||||
|
@ -270,6 +279,7 @@ export function CallsList({
|
|||
title={
|
||||
<span className="CallsList__LoadingText CallsList__LoadingText--title" />
|
||||
}
|
||||
subtitleMaxLines={1}
|
||||
subtitle={
|
||||
<span className="CallsList__LoadingText CallsList__LoadingText--subtitle" />
|
||||
}
|
||||
|
@ -306,6 +316,7 @@ export function CallsList({
|
|||
style={style}
|
||||
className={classNames('CallsList__Item', {
|
||||
'CallsList__Item--selected': isSelected,
|
||||
'CallsList__Item--missed': wasMissed,
|
||||
})}
|
||||
>
|
||||
<ListTile
|
||||
|
@ -320,17 +331,22 @@ export function CallsList({
|
|||
isMe={false}
|
||||
title={conversation.title}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
badge={undefined}
|
||||
className="CallsList__ItemAvatar"
|
||||
/>
|
||||
}
|
||||
trailing={
|
||||
<span
|
||||
className={classNames('CallsList__ItemIcon', {
|
||||
'CallsList__ItemIcon--Phone': item.type === CallType.Audio,
|
||||
'CallsList__ItemIcon--Video': item.type !== CallType.Audio,
|
||||
})}
|
||||
<CallsNewCallButton
|
||||
callType={item.type}
|
||||
hasActiveCall={hasActiveCall}
|
||||
onClick={() => {
|
||||
if (item.type === CallType.Audio) {
|
||||
onOutgoingAudioCallInConversation(conversation.id);
|
||||
} else {
|
||||
onOutgoingVideoCallInConversation(conversation.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
|
@ -341,12 +357,9 @@ export function CallsList({
|
|||
<UserText text={conversation.title} />
|
||||
</span>
|
||||
}
|
||||
subtitleMaxLines={1}
|
||||
subtitle={
|
||||
<span
|
||||
className={classNames('CallsList__ItemCallInfo', {
|
||||
'CallsList__ItemCallInfo--missed': wasMissed,
|
||||
})}
|
||||
>
|
||||
<span className="CallsList__ItemCallInfo">
|
||||
{item.children.length > 1 ? `(${item.children.length}) ` : ''}
|
||||
{statusText} ·{' '}
|
||||
<Timestamp i18n={i18n} timestamp={item.timestamp} />
|
||||
|
@ -360,10 +373,13 @@ export function CallsList({
|
|||
);
|
||||
},
|
||||
[
|
||||
hasActiveCall,
|
||||
searchState,
|
||||
getConversation,
|
||||
selectedCallHistoryGroup,
|
||||
onSelectCallHistoryGroup,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
i18n,
|
||||
]
|
||||
);
|
||||
|
@ -402,21 +418,28 @@ export function CallsList({
|
|||
onClear={handleSearchInputClear}
|
||||
value={queryInput}
|
||||
/>
|
||||
<button
|
||||
className={classNames('CallsList__ToggleFilterByMissed', {
|
||||
'CallsList__ToggleFilterByMissed--pressed': filteringByMissed,
|
||||
})}
|
||||
type="button"
|
||||
aria-pressed={filteringByMissed}
|
||||
aria-roledescription={i18n(
|
||||
'icu:CallsList__ToggleFilterByMissed__RoleDescription'
|
||||
)}
|
||||
onClick={handleStatusToggle}
|
||||
<Tooltip
|
||||
direction={TooltipPlacement.Bottom}
|
||||
content={i18n('icu:CallsList__ToggleFilterByMissedLabel')}
|
||||
theme={Theme.Dark}
|
||||
delay={600}
|
||||
>
|
||||
<span className="CallsList__ToggleFilterByMissedLabel">
|
||||
{i18n('icu:CallsList__ToggleFilterByMissedLabel')}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={classNames('CallsList__ToggleFilterByMissed', {
|
||||
'CallsList__ToggleFilterByMissed--pressed': filteringByMissed,
|
||||
})}
|
||||
type="button"
|
||||
aria-pressed={filteringByMissed}
|
||||
aria-roledescription={i18n(
|
||||
'icu:CallsList__ToggleFilterByMissed__RoleDescription'
|
||||
)}
|
||||
onClick={handleStatusToggle}
|
||||
>
|
||||
<span className="CallsList__ToggleFilterByMissedLabel">
|
||||
{i18n('icu:CallsList__ToggleFilterByMissedLabel')}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</NavSidebarSearchHeader>
|
||||
|
||||
{hasEmptyResults && (
|
||||
|
|
|
@ -16,11 +16,11 @@ import { strictAssert } from '../util/assert';
|
|||
import { UserText } from './UserText';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { Intl } from './Intl';
|
||||
import type { ActiveCallStateType } from '../state/ducks/calling';
|
||||
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||
import { CallType } from '../types/CallDisposition';
|
||||
|
||||
type CallsNewCallProps = Readonly<{
|
||||
activeCall: ActiveCallStateType | undefined;
|
||||
hasActiveCall: boolean;
|
||||
allConversations: ReadonlyArray<ConversationType>;
|
||||
i18n: LocalizerType;
|
||||
onSelectConversation: (conversationId: string) => void;
|
||||
|
@ -33,8 +33,39 @@ type Row =
|
|||
| { kind: 'header'; title: string }
|
||||
| { kind: 'conversation'; conversation: ConversationType };
|
||||
|
||||
export function CallsNewCallButton({
|
||||
callType,
|
||||
hasActiveCall,
|
||||
onClick,
|
||||
}: {
|
||||
callType: CallType;
|
||||
hasActiveCall: boolean;
|
||||
onClick: () => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="CallsNewCall__ItemActionButton"
|
||||
aria-disabled={hasActiveCall}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
if (!hasActiveCall) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{callType === CallType.Audio && (
|
||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Phone" />
|
||||
)}
|
||||
{callType !== CallType.Audio && (
|
||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Video" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallsNewCall({
|
||||
activeCall,
|
||||
hasActiveCall,
|
||||
allConversations,
|
||||
i18n,
|
||||
onSelectConversation,
|
||||
|
@ -146,8 +177,6 @@ export function CallsNewCall({
|
|||
);
|
||||
}
|
||||
|
||||
const callButtonsDisabled = activeCall != null;
|
||||
|
||||
return (
|
||||
<div key={key} style={style}>
|
||||
<ListTile
|
||||
|
@ -168,33 +197,22 @@ export function CallsNewCall({
|
|||
trailing={
|
||||
<div className="CallsNewCall__ItemActions">
|
||||
{item.conversation.type === 'direct' && (
|
||||
<button
|
||||
type="button"
|
||||
className="CallsNewCall__ItemActionButton"
|
||||
aria-disabled={callButtonsDisabled}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
if (!callButtonsDisabled) {
|
||||
onOutgoingAudioCallInConversation(item.conversation.id);
|
||||
}
|
||||
<CallsNewCallButton
|
||||
callType={CallType.Audio}
|
||||
hasActiveCall={hasActiveCall}
|
||||
onClick={() => {
|
||||
onOutgoingAudioCallInConversation(item.conversation.id);
|
||||
}}
|
||||
>
|
||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Phone" />
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="CallsNewCall__ItemActionButton"
|
||||
aria-disabled={callButtonsDisabled}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
if (!callButtonsDisabled) {
|
||||
onOutgoingVideoCallInConversation(item.conversation.id);
|
||||
}
|
||||
<CallsNewCallButton
|
||||
// It's okay if this is a group
|
||||
callType={CallType.Video}
|
||||
hasActiveCall={hasActiveCall}
|
||||
onClick={() => {
|
||||
onOutgoingVideoCallInConversation(item.conversation.id);
|
||||
}}
|
||||
>
|
||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Video" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
onClick={() => {
|
||||
|
@ -207,7 +225,7 @@ export function CallsNewCall({
|
|||
[
|
||||
rows,
|
||||
i18n,
|
||||
activeCall,
|
||||
hasActiveCall,
|
||||
onSelectConversation,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
|
|
|
@ -198,18 +198,25 @@ export function CallsTab({
|
|||
{sidebarView === CallsTabSidebarView.CallsListView && (
|
||||
<CallsList
|
||||
key={CallsTabSidebarView.CallsListView}
|
||||
hasActiveCall={activeCall != null}
|
||||
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
|
||||
getCallHistoryGroups={getCallHistoryGroups}
|
||||
getConversation={getConversation}
|
||||
i18n={i18n}
|
||||
selectedCallHistoryGroup={selected?.callHistoryGroup ?? null}
|
||||
onSelectCallHistoryGroup={handleSelectCallHistoryGroup}
|
||||
onOutgoingAudioCallInConversation={
|
||||
handleOutgoingAudioCallInConversation
|
||||
}
|
||||
onOutgoingVideoCallInConversation={
|
||||
handleOutgoingVideoCallInConversation
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{sidebarView === CallsTabSidebarView.NewCallView && (
|
||||
<CallsNewCall
|
||||
key={CallsTabSidebarView.NewCallView}
|
||||
activeCall={activeCall}
|
||||
hasActiveCall={activeCall != null}
|
||||
allConversations={allConversations}
|
||||
i18n={i18n}
|
||||
regionCode={regionCode}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
|
@ -26,8 +26,7 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
|||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import type { DurationInSeconds } from '../util/durations';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
import { getNavSidebarWidthBreakpoint } from './_util';
|
||||
import { WidthBreakpoint, getNavSidebarWidthBreakpoint } from './_util';
|
||||
import * as KeyboardLayout from '../services/keyboardLayout';
|
||||
import type { LookupConversationWithoutServiceIdActionsType } from '../util/lookupConversationWithoutServiceId';
|
||||
import type { ShowConversationType } from '../state/ducks/conversations';
|
||||
|
@ -42,7 +41,7 @@ import type {
|
|||
ReplaceAvatarActionType,
|
||||
SaveAvatarToDiskActionType,
|
||||
} from '../types/Avatar';
|
||||
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||
import { useSizeObserver } from '../hooks/useSizeObserver';
|
||||
import {
|
||||
NavSidebar,
|
||||
NavSidebarActionButton,
|
||||
|
@ -479,7 +478,12 @@ export function LeftPane({
|
|||
// It also ensures that we scroll to the top when switching views.
|
||||
const listKey = preRowsNode ? 1 : 0;
|
||||
|
||||
const widthBreakpoint = getNavSidebarWidthBreakpoint(300);
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
const measureSize = useSizeObserver(measureRef);
|
||||
|
||||
const widthBreakpoint = getNavSidebarWidthBreakpoint(
|
||||
measureSize?.width ?? preferredWidthFromStorage
|
||||
);
|
||||
|
||||
const commonDialogProps = {
|
||||
i18n,
|
||||
|
@ -548,7 +552,7 @@ export function LeftPane({
|
|||
navTabsCollapsed={navTabsCollapsed}
|
||||
onToggleNavTabsCollapse={toggleNavTabsCollapse}
|
||||
preferredLeftPaneWidth={preferredWidthFromStorage}
|
||||
requiresFullWidth={false}
|
||||
requiresFullWidth={modeSpecificProps.mode !== LeftPaneMode.Inbox}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
actions={
|
||||
<>
|
||||
|
@ -603,91 +607,88 @@ export function LeftPane({
|
|||
showChooseGroupMembers,
|
||||
})}
|
||||
</div>
|
||||
<NavSidebarSearchHeader>
|
||||
{helper.getSearchInput({
|
||||
clearConversationSearch,
|
||||
clearSearch,
|
||||
i18n,
|
||||
onChangeComposeSearchTerm: event => {
|
||||
setComposeSearchTerm(event.target.value);
|
||||
},
|
||||
updateSearchTerm,
|
||||
showConversation,
|
||||
})}
|
||||
</NavSidebarSearchHeader>
|
||||
{(widthBreakpoint === WidthBreakpoint.Wide ||
|
||||
modeSpecificProps.mode !== LeftPaneMode.Inbox) && (
|
||||
<NavSidebarSearchHeader>
|
||||
{helper.getSearchInput({
|
||||
clearConversationSearch,
|
||||
clearSearch,
|
||||
i18n,
|
||||
onChangeComposeSearchTerm: event => {
|
||||
setComposeSearchTerm(event.target.value);
|
||||
},
|
||||
updateSearchTerm,
|
||||
showConversation,
|
||||
})}
|
||||
</NavSidebarSearchHeader>
|
||||
)}
|
||||
<div className="module-left-pane__dialogs">
|
||||
{dialogs.map(({ key, dialog }) => (
|
||||
<React.Fragment key={key}>{dialog}</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{preRowsNode && <React.Fragment key={0}>{preRowsNode}</React.Fragment>}
|
||||
<SizeObserver>
|
||||
{(ref, size) => (
|
||||
<div className="module-left-pane__list--measure" ref={ref}>
|
||||
<div className="module-left-pane__list--wrapper">
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="module-left-pane__list"
|
||||
data-supertab
|
||||
key={listKey}
|
||||
role="presentation"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ConversationList
|
||||
dimensions={size ?? undefined}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
getRow={getRow}
|
||||
i18n={i18n}
|
||||
onClickArchiveButton={showArchivedConversations}
|
||||
onClickContactCheckbox={(
|
||||
conversationId: string,
|
||||
disabledReason: undefined | ContactCheckboxDisabledReason
|
||||
) => {
|
||||
switch (disabledReason) {
|
||||
case undefined:
|
||||
toggleConversationInChooseMembers(conversationId);
|
||||
break;
|
||||
case ContactCheckboxDisabledReason.AlreadyAdded:
|
||||
case ContactCheckboxDisabledReason.MaximumContactsSelected:
|
||||
// These are no-ops.
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(disabledReason);
|
||||
}
|
||||
}}
|
||||
showUserNotFoundModal={showUserNotFoundModal}
|
||||
setIsFetchingUUID={setIsFetchingUUID}
|
||||
lookupConversationWithoutServiceId={
|
||||
lookupConversationWithoutServiceId
|
||||
}
|
||||
showConversation={showConversation}
|
||||
blockConversation={blockConversation}
|
||||
onSelectConversation={onSelectConversation}
|
||||
onOutgoingAudioCallInConversation={
|
||||
onOutgoingAudioCallInConversation
|
||||
}
|
||||
onOutgoingVideoCallInConversation={
|
||||
onOutgoingVideoCallInConversation
|
||||
}
|
||||
removeConversation={
|
||||
isContactManagementEnabled
|
||||
? removeConversation
|
||||
: undefined
|
||||
}
|
||||
renderMessageSearchResult={renderMessageSearchResult}
|
||||
rowCount={helper.getRowCount()}
|
||||
scrollBehavior={scrollBehavior}
|
||||
scrollToRowIndex={rowIndexToScrollTo}
|
||||
scrollable={isScrollable}
|
||||
shouldRecomputeRowHeights={shouldRecomputeRowHeights}
|
||||
showChooseGroupMembers={showChooseGroupMembers}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-left-pane__list--measure" ref={measureRef}>
|
||||
<div className="module-left-pane__list--wrapper">
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="module-left-pane__list"
|
||||
data-supertab
|
||||
key={listKey}
|
||||
role="presentation"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ConversationList
|
||||
dimensions={measureSize ?? undefined}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
getRow={getRow}
|
||||
i18n={i18n}
|
||||
onClickArchiveButton={showArchivedConversations}
|
||||
onClickContactCheckbox={(
|
||||
conversationId: string,
|
||||
disabledReason: undefined | ContactCheckboxDisabledReason
|
||||
) => {
|
||||
switch (disabledReason) {
|
||||
case undefined:
|
||||
toggleConversationInChooseMembers(conversationId);
|
||||
break;
|
||||
case ContactCheckboxDisabledReason.AlreadyAdded:
|
||||
case ContactCheckboxDisabledReason.MaximumContactsSelected:
|
||||
// These are no-ops.
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(disabledReason);
|
||||
}
|
||||
}}
|
||||
showUserNotFoundModal={showUserNotFoundModal}
|
||||
setIsFetchingUUID={setIsFetchingUUID}
|
||||
lookupConversationWithoutServiceId={
|
||||
lookupConversationWithoutServiceId
|
||||
}
|
||||
showConversation={showConversation}
|
||||
blockConversation={blockConversation}
|
||||
onSelectConversation={onSelectConversation}
|
||||
onOutgoingAudioCallInConversation={
|
||||
onOutgoingAudioCallInConversation
|
||||
}
|
||||
onOutgoingVideoCallInConversation={
|
||||
onOutgoingVideoCallInConversation
|
||||
}
|
||||
removeConversation={
|
||||
isContactManagementEnabled ? removeConversation : undefined
|
||||
}
|
||||
renderMessageSearchResult={renderMessageSearchResult}
|
||||
rowCount={helper.getRowCount()}
|
||||
scrollBehavior={scrollBehavior}
|
||||
scrollToRowIndex={rowIndexToScrollTo}
|
||||
scrollable={isScrollable}
|
||||
shouldRecomputeRowHeights={shouldRecomputeRowHeights}
|
||||
showChooseGroupMembers={showChooseGroupMembers}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SizeObserver>
|
||||
</div>
|
||||
</div>
|
||||
{footerContents && (
|
||||
<div className="module-left-pane__footer">{footerContents}</div>
|
||||
)}
|
||||
|
|
|
@ -15,27 +15,38 @@ 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';
|
||||
|
||||
type NavTabProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
badge?: ReactNode;
|
||||
iconClassName: string;
|
||||
id: NavTab;
|
||||
label: string;
|
||||
}>;
|
||||
|
||||
function NavTabsItem({ badge, iconClassName, id, label }: NavTabProps) {
|
||||
function NavTabsItem({ i18n, badge, iconClassName, id, label }: NavTabProps) {
|
||||
const isRTL = i18n.getLocaleDirection() === 'rtl';
|
||||
return (
|
||||
<Tab id={id} data-testid={`NavTabsItem--${id}`} className="NavTabs__Item">
|
||||
<span className="NavTabs__ItemLabel">{label}</span>
|
||||
<span className="NavTabs__ItemButton">
|
||||
<span className="NavTabs__ItemContent">
|
||||
<span
|
||||
role="presentation"
|
||||
className={`NavTabs__ItemIcon ${iconClassName}`}
|
||||
/>
|
||||
{badge && <span className="NavTabs__ItemBadge">{badge}</span>}
|
||||
<Tooltip
|
||||
content={label}
|
||||
theme={Theme.Dark}
|
||||
direction={isRTL ? TooltipPlacement.Left : TooltipPlacement.Right}
|
||||
delay={600}
|
||||
>
|
||||
<span className="NavTabs__ItemButton">
|
||||
<span className="NavTabs__ItemContent">
|
||||
<span
|
||||
role="presentation"
|
||||
className={`NavTabs__ItemIcon ${iconClassName}`}
|
||||
/>
|
||||
{badge && <span className="NavTabs__ItemBadge">{badge}</span>}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
|
@ -59,23 +70,30 @@ export function NavTabsToggle({
|
|||
function handleToggle() {
|
||||
onToggleNavTabsCollapse(!navTabsCollapsed);
|
||||
}
|
||||
const label = navTabsCollapsed
|
||||
? i18n('icu:NavTabsToggle__showTabs')
|
||||
: i18n('icu:NavTabsToggle__hideTabs');
|
||||
const isRTL = i18n.getLocaleDirection() === 'rtl';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="NavTabs__Item NavTabs__Toggle"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<span className="NavTabs__ItemButton">
|
||||
<span
|
||||
role="presentation"
|
||||
className="NavTabs__ItemIcon NavTabs__ItemIcon--Menu"
|
||||
/>
|
||||
<span className="NavTabs__ItemLabel">
|
||||
{navTabsCollapsed
|
||||
? i18n('icu:NavTabsToggle__showTabs')
|
||||
: i18n('icu:NavTabsToggle__hideTabs')}
|
||||
<Tooltip
|
||||
content={label}
|
||||
theme={Theme.Dark}
|
||||
direction={isRTL ? TooltipPlacement.Left : TooltipPlacement.Right}
|
||||
delay={600}
|
||||
>
|
||||
<span className="NavTabs__ItemButton">
|
||||
<span
|
||||
role="presentation"
|
||||
className="NavTabs__ItemIcon NavTabs__ItemIcon--Menu"
|
||||
/>
|
||||
<span className="NavTabs__ItemLabel">{label}</span>
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -127,6 +145,8 @@ export function NavTabs({
|
|||
onNavTabSelected(key as NavTab);
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -202,6 +222,7 @@ export function NavTabs({
|
|||
onSelectionChange={handleSelectionChange}
|
||||
>
|
||||
<NavTabsItem
|
||||
i18n={i18n}
|
||||
id={NavTab.Chats}
|
||||
label="Chats"
|
||||
iconClassName="NavTabs__ItemIcon--Chats"
|
||||
|
@ -226,12 +247,14 @@ export function NavTabs({
|
|||
}
|
||||
/>
|
||||
<NavTabsItem
|
||||
i18n={i18n}
|
||||
id={NavTab.Calls}
|
||||
label="Calls"
|
||||
iconClassName="NavTabs__ItemIcon--Calls"
|
||||
/>
|
||||
{storiesEnabled && (
|
||||
<NavTabsItem
|
||||
i18n={i18n}
|
||||
id={NavTab.Stories}
|
||||
label="Stories"
|
||||
iconClassName="NavTabs__ItemIcon--Stories"
|
||||
|
@ -252,49 +275,63 @@ export function NavTabs({
|
|||
className="NavTabs__Item"
|
||||
onClick={onShowSettings}
|
||||
>
|
||||
<span className="NavTabs__ItemButton">
|
||||
<span
|
||||
role="presentation"
|
||||
className="NavTabs__ItemIcon NavTabs__ItemIcon--Settings"
|
||||
/>
|
||||
<span className="NavTabs__ItemLabel">
|
||||
{i18n('icu:NavTabs__ItemLabel--Settings')}
|
||||
<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>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="NavTabs__Item"
|
||||
className="NavTabs__Item NavTabs__Item--Profile"
|
||||
data-supertab
|
||||
onClick={() => {
|
||||
setShowAvatarPopup(true);
|
||||
}}
|
||||
aria-label={i18n('icu:NavTabs__ItemLabel--Profile')}
|
||||
>
|
||||
<span className="NavTabs__ItemButton" ref={setTargetElement}>
|
||||
<span className="NavTabs__ItemContent">
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarPath={me.avatarPath}
|
||||
badge={badge}
|
||||
className="module-main-header__avatar"
|
||||
color={me.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe
|
||||
phoneNumber={me.phoneNumber}
|
||||
profileName={me.profileName}
|
||||
theme={theme}
|
||||
title={me.title}
|
||||
// `sharedGroupNames` makes no sense for yourself, but
|
||||
// `<Avatar>` needs it to determine blurring.
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
/>
|
||||
{hasPendingUpdate && <div className="NavTabs__AvatarBadge" />}
|
||||
<Tooltip
|
||||
content={i18n('icu:NavTabs__ItemLabel--Profile')}
|
||||
theme={Theme.Dark}
|
||||
direction={isRTL ? TooltipPlacement.Left : TooltipPlacement.Right}
|
||||
delay={600}
|
||||
>
|
||||
<span className="NavTabs__ItemButton" ref={setTargetElement}>
|
||||
<span className="NavTabs__ItemContent">
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarPath={me.avatarPath}
|
||||
badge={badge}
|
||||
className="module-main-header__avatar"
|
||||
color={me.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe
|
||||
phoneNumber={me.phoneNumber}
|
||||
profileName={me.profileName}
|
||||
theme={theme}
|
||||
title={me.title}
|
||||
// `sharedGroupNames` makes no sense for yourself, but
|
||||
// `<Avatar>` needs it to determine blurring.
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
/>
|
||||
{hasPendingUpdate && <div className="NavTabs__AvatarBadge" />}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</button>
|
||||
{showAvatarPopup &&
|
||||
portalElement != null &&
|
||||
|
|
|
@ -10,6 +10,7 @@ import { filterAndSortConversationsByRecent } from '../util/filterAndSortConvers
|
|||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { ConversationWithStoriesType } from '../state/selectors/conversations';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import type { PropsType as StoriesSettingsModalPropsType } from './StoriesSettingsModal';
|
||||
import {
|
||||
|
@ -34,7 +35,6 @@ import { MY_STORY_ID, getStoryDistributionListName } from '../types/Stories';
|
|||
import type { RenderModalPage, ModalPropsType } from './Modal';
|
||||
import { PagedModal, ModalPage } from './Modal';
|
||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||
import { Theme } from '../util/theme';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import { StoryImage } from './StoryImage';
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
|
@ -42,6 +42,7 @@ import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
|
|||
import { getStoryBackground } from '../util/getStoryBackground';
|
||||
import { makeObjectUrl, revokeObjectUrl } from '../types/VisualAttachment';
|
||||
import { UserText } from './UserText';
|
||||
import { Theme } from '../util/theme';
|
||||
|
||||
export type PropsType = {
|
||||
draftAttachment: AttachmentType;
|
||||
|
@ -70,6 +71,7 @@ export type PropsType = {
|
|||
conversationIds: Array<string>
|
||||
) => unknown;
|
||||
signalConnections: Array<ConversationType>;
|
||||
theme: ThemeType;
|
||||
toggleGroupsForStorySend: (cids: Array<string>) => Promise<void>;
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList: Record<
|
||||
string,
|
||||
|
@ -141,6 +143,7 @@ export function SendStoryModal({
|
|||
onViewersUpdated,
|
||||
setMyStoriesToAllSignalConnections,
|
||||
signalConnections,
|
||||
theme,
|
||||
toggleGroupsForStorySend,
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList,
|
||||
toggleSignalConnectionsModal,
|
||||
|
@ -402,6 +405,7 @@ export function SendStoryModal({
|
|||
setPage={setPage}
|
||||
setSelectedContacts={setSelectedContacts}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
theme={theme}
|
||||
onBackButtonClick={() =>
|
||||
confirmDiscardIf(selectedContacts.length > 0, () =>
|
||||
setListIdToEdit(undefined)
|
||||
|
@ -485,6 +489,7 @@ export function SendStoryModal({
|
|||
}
|
||||
selectedContacts={selectedContacts}
|
||||
setSelectedContacts={setSelectedContacts}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.ChooseGroups) {
|
||||
|
@ -700,7 +705,7 @@ export function SendStoryModal({
|
|||
placement: 'bottom',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light}
|
||||
>
|
||||
<label
|
||||
className="SendStoryModal__distribution-list__label"
|
||||
|
@ -816,7 +821,7 @@ export function SendStoryModal({
|
|||
placement: 'bottom',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light}
|
||||
>
|
||||
<label
|
||||
className="SendStoryModal__distribution-list__label"
|
||||
|
@ -913,7 +918,7 @@ export function SendStoryModal({
|
|||
placement: 'bottom',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light}
|
||||
>
|
||||
{({ openMenu, onKeyDown, ref, menuNode }) => (
|
||||
<div>
|
||||
|
@ -947,7 +952,7 @@ export function SendStoryModal({
|
|||
{!confirmDiscardModal && (
|
||||
<PagedModal
|
||||
modalName="SendStoryModal"
|
||||
theme={Theme.Dark}
|
||||
theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light}
|
||||
onClose={() => confirmDiscardIf(selectedContacts.length > 0, onClose)}
|
||||
>
|
||||
{modal}
|
||||
|
@ -958,7 +963,7 @@ export function SendStoryModal({
|
|||
body={i18n('icu:SendStoryModal__announcements-only')}
|
||||
i18n={i18n}
|
||||
onClose={() => setHasAnnouncementsOnlyAlert(false)}
|
||||
theme={Theme.Dark}
|
||||
theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light}
|
||||
/>
|
||||
)}
|
||||
{confirmRemoveGroupId && (
|
||||
|
@ -978,7 +983,7 @@ export function SendStoryModal({
|
|||
onClose={() => {
|
||||
setConfirmRemoveGroupId(undefined);
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light}
|
||||
>
|
||||
{i18n('icu:SendStoryModal__confirm-remove-group')}
|
||||
</ConfirmationDialog>
|
||||
|
@ -1000,7 +1005,7 @@ export function SendStoryModal({
|
|||
onClose={() => {
|
||||
setConfirmDeleteList(undefined);
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
theme={theme === ThemeType.dark ? Theme.Dark : Theme.Light}
|
||||
>
|
||||
{i18n('icu:StoriesSettings__delete-list--confirm', {
|
||||
name: confirmDeleteList.name,
|
||||
|
|
|
@ -7,7 +7,7 @@ import { noop } from 'lodash';
|
|||
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { ConversationWithStoriesType } from '../state/selectors/conversations';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import type { Row } from './ConversationList';
|
||||
import type { StoryDistributionListWithMembersDataType } from '../types/Stories';
|
||||
|
@ -27,8 +27,6 @@ import { MY_STORY_ID, getStoryDistributionListName } from '../types/Stories';
|
|||
import { PagedModal, ModalPage } from './Modal';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||
import { Theme } from '../util/theme';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import {
|
||||
|
@ -68,6 +66,7 @@ export type PropsType = {
|
|||
) => unknown;
|
||||
setMyStoriesToAllSignalConnections: () => unknown;
|
||||
storyViewReceiptsEnabled: boolean;
|
||||
theme: ThemeType;
|
||||
toggleSignalConnectionsModal: () => unknown;
|
||||
setStoriesDisabled: (value: boolean) => void;
|
||||
getConversationByUuid: (
|
||||
|
@ -257,6 +256,7 @@ export function StoriesSettingsModal({
|
|||
setMyStoriesToAllSignalConnections,
|
||||
storyViewReceiptsEnabled,
|
||||
toggleSignalConnectionsModal,
|
||||
theme,
|
||||
setStoriesDisabled,
|
||||
getConversationByUuid,
|
||||
}: PropsType): JSX.Element {
|
||||
|
@ -347,6 +347,7 @@ export function StoriesSettingsModal({
|
|||
}}
|
||||
selectedContacts={selectedContacts}
|
||||
setSelectedContacts={setSelectedContacts}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if (listToEdit) {
|
||||
|
@ -364,6 +365,7 @@ export function StoriesSettingsModal({
|
|||
setPage={setPage}
|
||||
setSelectedContacts={setSelectedContacts}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
theme={theme}
|
||||
onBackButtonClick={() => setListToEditId(undefined)}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
|
@ -479,7 +481,6 @@ export function StoriesSettingsModal({
|
|||
<PagedModal
|
||||
modalName="StoriesSettingsModal"
|
||||
moduleClassName="StoriesSettingsModal"
|
||||
theme={Theme.Dark}
|
||||
onClose={() =>
|
||||
confirmDiscardIf(selectedContacts.length > 0, hideStoriesSettings)
|
||||
}
|
||||
|
@ -504,7 +505,6 @@ export function StoriesSettingsModal({
|
|||
onClose={() => {
|
||||
setConfirmDeleteList(undefined);
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
{i18n('icu:StoriesSettings__delete-list--confirm', {
|
||||
name: confirmDeleteList.name,
|
||||
|
@ -529,7 +529,6 @@ export function StoriesSettingsModal({
|
|||
onClose={() => {
|
||||
setConfirmRemoveGroup(null);
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
{i18n('icu:StoriesSettings__remove_group--confirm', {
|
||||
groupTitle: confirmRemoveGroup.title,
|
||||
|
@ -551,6 +550,7 @@ type DistributionListSettingsModalPropsType = {
|
|||
}) => unknown;
|
||||
setPage: (page: Page) => unknown;
|
||||
setSelectedContacts: (contacts: Array<ConversationType>) => unknown;
|
||||
theme: ThemeType;
|
||||
onBackButtonClick: (() => void) | undefined;
|
||||
onClose: () => void;
|
||||
} & Pick<
|
||||
|
@ -574,6 +574,7 @@ export function DistributionListSettingsModal({
|
|||
setMyStoriesToAllSignalConnections,
|
||||
setPage,
|
||||
setSelectedContacts,
|
||||
theme,
|
||||
toggleSignalConnectionsModal,
|
||||
signalConnectionsCount,
|
||||
}: DistributionListSettingsModalPropsType): JSX.Element {
|
||||
|
@ -680,7 +681,7 @@ export function DistributionListSettingsModal({
|
|||
isMe
|
||||
sharedGroupNames={member.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
theme={ThemeType.dark}
|
||||
theme={theme}
|
||||
title={member.title}
|
||||
/>
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
|
@ -756,7 +757,6 @@ export function DistributionListSettingsModal({
|
|||
onClose={() => {
|
||||
setConfirmRemoveMember(undefined);
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
title={i18n('icu:StoriesSettings__remove--title', {
|
||||
title: confirmRemoveMember.title,
|
||||
})}
|
||||
|
@ -960,6 +960,7 @@ type EditDistributionListModalPropsType = {
|
|||
selectedContacts: Array<ConversationType>;
|
||||
onClose: () => unknown;
|
||||
setSelectedContacts: (contacts: Array<ConversationType>) => unknown;
|
||||
theme: ThemeType;
|
||||
onBackButtonClick: () => void;
|
||||
} & Pick<PropsType, 'candidateConversations' | 'getPreferredBadge' | 'i18n'>;
|
||||
|
||||
|
@ -973,6 +974,7 @@ export function EditDistributionListModal({
|
|||
onClose,
|
||||
selectedContacts,
|
||||
setSelectedContacts,
|
||||
theme,
|
||||
onBackButtonClick,
|
||||
}: EditDistributionListModalPropsType): JSX.Element {
|
||||
const [storyName, setStoryName] = useState('');
|
||||
|
@ -1090,7 +1092,7 @@ export function EditDistributionListModal({
|
|||
isMe
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
theme={ThemeType.dark}
|
||||
theme={theme}
|
||||
title={contact.title}
|
||||
/>
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
|
@ -1222,7 +1224,7 @@ export function EditDistributionListModal({
|
|||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
showConversation={shouldNeverBeCalled}
|
||||
showUserNotFoundModal={shouldNeverBeCalled}
|
||||
theme={ThemeType.dark}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -11,7 +11,7 @@ import type {
|
|||
} from '../types/Attachment';
|
||||
import type { LinkPreviewSourceType } from '../types/LinkPreview';
|
||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||
import type { Props as StickerButtonProps } from './stickers/StickerButton';
|
||||
import type { PropsType as SendStoryModalPropsType } from './SendStoryModal';
|
||||
import type { StoryDistributionIdString } from '../types/StoryDistributionId';
|
||||
|
@ -67,6 +67,7 @@ export type PropsType = {
|
|||
props: SmartCompositionTextAreaProps
|
||||
) => JSX.Element;
|
||||
sendStoryModalOpenStateChanged: (isOpen: boolean) => unknown;
|
||||
theme: ThemeType;
|
||||
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
|
||||
Pick<
|
||||
SendStoryModalPropsType,
|
||||
|
@ -134,6 +135,7 @@ export function StoryCreator({
|
|||
setMyStoriesToAllSignalConnections,
|
||||
signalConnections,
|
||||
skinTone,
|
||||
theme,
|
||||
toggleGroupsForStorySend,
|
||||
toggleSignalConnectionsModal,
|
||||
}: PropsType): JSX.Element | null {
|
||||
|
@ -228,6 +230,7 @@ export function StoryCreator({
|
|||
mostRecentActiveStoryTimestampByGroupOrDistributionList={
|
||||
mostRecentActiveStoryTimestampByGroupOrDistributionList
|
||||
}
|
||||
theme={theme}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { noop } from 'lodash';
|
||||
import { Manager, Reference, Popper } from 'react-popper';
|
||||
|
@ -89,8 +89,12 @@ export type PropsType = {
|
|||
sticky?: boolean;
|
||||
theme?: Theme;
|
||||
wrapperClassName?: string;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
let GLOBAL_EXIT_TIMER: NodeJS.Timeout | undefined;
|
||||
let GLOBAL_TOOLTIP_DISABLE_DELAY = false;
|
||||
|
||||
export function Tooltip({
|
||||
children,
|
||||
className,
|
||||
|
@ -100,15 +104,56 @@ export function Tooltip({
|
|||
theme,
|
||||
popperModifiers = [],
|
||||
wrapperClassName,
|
||||
delay,
|
||||
}: PropsType): JSX.Element {
|
||||
const [isHovering, setIsHovering] = React.useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | undefined>();
|
||||
const [active, setActive] = React.useState(false);
|
||||
|
||||
const showTooltip = isHovering || Boolean(sticky);
|
||||
const showTooltip = active || Boolean(sticky);
|
||||
|
||||
const tooltipThemeClassName = theme
|
||||
? `module-tooltip--${themeClassName(theme)}`
|
||||
: undefined;
|
||||
|
||||
function handleHoverChanged(hovering: boolean) {
|
||||
// Don't accept updates that aren't valid anymore
|
||||
clearTimeout(GLOBAL_EXIT_TIMER);
|
||||
clearTimeout(timeoutRef.current);
|
||||
|
||||
// We can skip past all of this if there's no delay
|
||||
if (delay != null) {
|
||||
// If we're now hovering, and delays haven't been disabled globally
|
||||
// we should start the timer to show the tooltip
|
||||
if (hovering && !GLOBAL_TOOLTIP_DISABLE_DELAY) {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setActive(true);
|
||||
// Since we have shown a tooltip we can now disable these delays
|
||||
// globally.
|
||||
GLOBAL_TOOLTIP_DISABLE_DELAY = true;
|
||||
}, delay);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hovering) {
|
||||
// If we're not hovering, we should hide the tooltip immediately
|
||||
setActive(false);
|
||||
|
||||
// If we've disabled delays globally, we need to start a timer to undo
|
||||
// that after some time has passed.
|
||||
if (GLOBAL_TOOLTIP_DISABLE_DELAY) {
|
||||
GLOBAL_EXIT_TIMER = setTimeout(() => {
|
||||
GLOBAL_TOOLTIP_DISABLE_DELAY = false;
|
||||
|
||||
// We're always going to use 300 here so that a tooltip with a really
|
||||
// long delay doesn't affect all of the others
|
||||
}, 300);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
setActive(hovering);
|
||||
}
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
|
@ -116,7 +161,7 @@ export function Tooltip({
|
|||
<TooltipEventWrapper
|
||||
className={wrapperClassName}
|
||||
ref={ref}
|
||||
onHoverChanged={setIsHovering}
|
||||
onHoverChanged={handleHoverChanged}
|
||||
>
|
||||
{children}
|
||||
</TooltipEventWrapper>
|
||||
|
|
|
@ -462,10 +462,7 @@ export function ConversationDetails({
|
|||
</div>
|
||||
|
||||
{callHistoryGroup && (
|
||||
<PanelSection>
|
||||
<h2 className="ConversationDetails__CallHistoryGroup__header">
|
||||
{formatDate(i18n, callHistoryGroup.timestamp)}
|
||||
</h2>
|
||||
<PanelSection title={formatDate(i18n, callHistoryGroup.timestamp)}>
|
||||
<ol className="ConversationDetails__CallHistoryGroup__List">
|
||||
{callHistoryGroup.children.map(child => {
|
||||
return (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue