Update call tab design based on feedback

This commit is contained in:
Jamie Kyle 2023-08-10 15:16:51 -07:00 committed by Jamie Kyle
parent ce28993c78
commit 3268d3e6eb
32 changed files with 601 additions and 289 deletions

View file

@ -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,

View file

@ -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} &middot;{' '}
<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 && (

View file

@ -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,

View file

@ -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}

View file

@ -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>
)}

View file

@ -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 &&

View file

@ -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,

View file

@ -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>
)}

View file

@ -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}
/>
)}

View file

@ -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>

View file

@ -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 (