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

@ -6684,7 +6684,7 @@
"description": "Calls Tab > Calls List > Search Input > Placeholder"
},
"icu:CallsList__ToggleFilterByMissedLabel": {
"messageformat": "Toggle filter by missed",
"messageformat": "Filter by missed",
"description": "Calls Tab > Calls List > Toggle search filter by missed > Accessibility label"
},
"icu:CallsList__ToggleFilterByMissed__RoleDescription": {

View file

@ -868,6 +868,7 @@ $rtl-icon-map: (
}
@mixin NavTabs__Scroller {
padding-bottom: 8px;
@include scrollbar;
&::-webkit-scrollbar-thumb {
@include light-theme {

View file

@ -6974,7 +6974,8 @@ button.module-image__border-overlay:focus {
content: '';
display: block;
height: 0;
margin-inline-start: -6px;
/* stylelint-disable-next-line liberty/use-logical-spec */
margin-left: -6px;
margin-top: -6px;
position: absolute;
width: 0;
@ -6993,12 +6994,15 @@ button.module-image__border-overlay:focus {
&[data-placement='right'] {
.module-tooltip-arrow {
inset-inline-start: 0;
/* stylelint-disable-next-line liberty/use-logical-spec */
left: 0;
}
.module-tooltip-arrow::after {
inset-inline-start: -6px;
border-inline-end-color: var(--tooltip-background-color);
/* stylelint-disable-next-line liberty/use-logical-spec */
left: -6px;
/* stylelint-disable-next-line liberty/use-logical-spec */
border-right-color: var(--tooltip-background-color);
}
}
@ -7015,12 +7019,15 @@ button.module-image__border-overlay:focus {
&[data-placement='left'] {
.module-tooltip-arrow {
inset-inline-end: 0;
/* stylelint-disable-next-line liberty/use-logical-spec */
right: 0;
}
.module-tooltip-arrow::after {
inset-inline-end: -12px;
border-inline-start-color: var(--tooltip-background-color);
/* stylelint-disable-next-line liberty/use-logical-spec */
right: -12px;
/* stylelint-disable-next-line liberty/use-logical-spec */
border-left-color: var(--tooltip-background-color);
}
}
}

View file

@ -286,4 +286,5 @@ $z-index-above-context-menu: 126;
$NavTabs__width: 80px;
// These values are 'block' specific to coordinate with the NavSidebar__Header
$NavTabs__Item__blockPadding: 2px;
$NavTabs__Toggle__blockPadding: 8px;
$NavTabs__ItemButton__blockPadding: 10px;

View file

@ -52,6 +52,8 @@
width: 100%;
height: 100%;
padding-block: 80px;
padding-inline: 24px;
user-select: none;
}
.CallsTab__ClearCallHistoryIcon {
@ -183,8 +185,8 @@
.CallsList__LoadingAvatar {
display: block;
width: 32px;
height: 32px;
width: 36px;
height: 36px;
border-radius: 9999px;
}
@ -206,10 +208,20 @@
font-weight: bold;
}
.CallsList__ItemCallInfo--missed {
.CallsList__ItemCallInfo {
@include font-body-1;
}
// Override .ListTile__subtitle so ellipsis is correct color
.CallsList__Item--missed .ListTile__subtitle {
color: $color-accent-red;
}
// Override .ListTile
.ListTile.CallsList__ItemTile {
padding-block: 12px;
}
.CallsList__Item--selected .CallsList__ItemTile {
@include light-theme {
background-color: $color-gray-15;

View file

@ -99,7 +99,6 @@
&__popper--single-item &__option {
padding-block: 12px;
padding-inline: 6px;
}
&__divider {

View file

@ -30,6 +30,7 @@
justify-content: center;
padding-bottom: 8px;
padding-top: 12px;
user-select: text;
}
&__subtitle {
@ -41,6 +42,11 @@
@include dark-theme {
color: $color-gray-25;
}
&__about,
&__phone-number {
user-select: text;
}
}
&__root--editable &__title {
@ -513,11 +519,6 @@
}
}
.ConversationDetails__CallHistoryGroup__header {
@include font-title-2;
margin-block: 24px 16px;
}
.ConversationDetails__CallHistoryGroup__List {
list-style: none;
margin: 0;

View file

@ -20,6 +20,7 @@
&__body {
margin-top: calc(#{$header-height} + var(--title-bar-drag-area-height));
padding-inline: 24px;
}
&__header {

View file

@ -7,6 +7,7 @@
-webkit-line-clamp: 2;
display: -webkit-box;
overflow: hidden;
user-select: text;
a {
@include light-theme {

View file

@ -125,6 +125,11 @@
@include sr-only;
}
.NavSidebar .module-SearchInput__container {
// override .module-SearchInput__container
margin: 0;
}
.NavSidebar__Content {
flex: 1 1 0%;
min-height: 0;

View file

@ -1,6 +1,10 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
$NavTabs__ToggleButton__blockPadding: 4px;
$NavTabs__ItemIcon__size: 20px;
$NavTabs__ProfileAvatar__size: 28px;
// This effectively wraps the entire app
.NavTabs__Container {
position: relative;
@ -14,6 +18,7 @@
display: flex;
flex-shrink: 0;
flex-direction: column;
align-items: center;
width: $NavTabs__width;
height: 100%;
padding-top: var(--title-bar-drag-area-height);
@ -43,6 +48,12 @@
// Handled by .NavTabs__ItemButton
outline: none;
}
&.NavTabs__Toggle {
padding-block: calc(
$NavTabs__Item__blockPadding + $NavTabs__ItemButton__blockPadding -
$NavTabs__ToggleButton__blockPadding
);
}
}
.NavTabs__ItemButton {
@ -50,7 +61,7 @@
display: flex;
align-items: center;
justify-content: center;
padding: $NavTabs__ItemButton__blockPadding;
padding-block: $NavTabs__ItemButton__blockPadding;
border-radius: 8px;
.NavTabs__Item:hover &,
.NavTabs__Item:focus-visible & {
@ -73,6 +84,21 @@
background: $color-gray-62;
}
}
.NavTabs__Toggle & {
width: fit-content;
padding: $NavTabs__ToggleButton__blockPadding;
margin-block: 0;
margin-inline: auto;
border-radius: 4px;
}
.NavTabs__Item--Profile & {
// Normalize for the size difference of the avatar vs sidebar icons
padding-block: calc(
$NavTabs__ItemButton__blockPadding -
(($NavTabs__ProfileAvatar__size - $NavTabs__ItemIcon__size) / 2)
);
}
}
.NavTabs__ItemContent {
@ -107,8 +133,8 @@
.NavTabs__ItemIcon {
display: block;
width: 20px;
height: 20px;
width: $NavTabs__ItemIcon__size;
height: $NavTabs__ItemIcon__size;
}
@mixin NavTabs__Icon($icon) {
@ -157,10 +183,18 @@
}
.NavTabs__TabList {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
flex: 1;
}
.NavTabs__Misc {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 8px;
}

View file

@ -5,6 +5,8 @@
&__container {
position: relative;
flex: 1 0 0;
margin-inline: 16px;
margin-bottom: 8px;
}
&__icon {

View file

@ -83,7 +83,6 @@
flex-direction: column;
flex: 1;
overflow-y: overlay;
padding-block: 0;
padding-inline: 16px;
&--empty {

View file

@ -45,7 +45,13 @@
&::before {
@include rounded-corners;
background: inherit;
border: 1.5px solid $color-gray-60;
border: 1.5px solid;
@include light-theme {
border-color: $color-gray-25;
}
@include dark-theme {
border-color: $color-gray-60;
}
content: '';
display: block;
height: 20px;
@ -80,7 +86,12 @@
&__viewers {
display: flex;
@include font-body-2;
color: $color-gray-25;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
&__left {
@ -96,13 +107,23 @@
align-items: center;
width: 32px;
height: 32px;
background: $color-gray-75;
@include light-theme {
background: $color-gray-15;
}
@include dark-theme {
background: $color-gray-75;
}
&::after {
@include color-svg($svg, $color-white);
content: '';
height: 20px;
width: 20px;
@include light-theme {
@include color-svg($svg, $color-black);
}
@include dark-theme {
@include color-svg($svg, $color-white);
}
}
}
@ -126,10 +147,22 @@
&__delete {
@include button-reset;
@include color-svg('../images/icons/v3/trash/trash.svg', $color-gray-25);
height: 20px;
width: 20px;
visibility: hidden;
@include light-theme {
@include color-svg(
'../images/icons/v3/trash/trash.svg',
$color-gray-45
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v3/trash/trash.svg',
$color-gray-25
);
}
}
&:hover &__delete {
@ -139,8 +172,14 @@
&__divider {
width: 100%;
border: 0 solid $color-gray-65;
border: 0 solid;
border-top-width: 1px;
@include light-theme {
border-color: $color-gray-15;
}
@include dark-theme {
border-color: $color-gray-65;
}
}
&__input__container {
@ -150,8 +189,13 @@
&__visibility {
@include font-subtitle;
color: $color-gray-25;
margin-top: 10px;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
&__title {
@ -161,9 +205,14 @@
&__description {
@include font-subtitle;
color: $color-gray-25;
margin-top: 0px;
margin-bottom: 16px;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
&__listHeader {
@ -215,7 +264,12 @@
}
&__checkbox-description {
color: $color-gray-25;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
&__conversation-list {
@ -226,11 +280,21 @@
&__disclaimer {
@include font-subtitle;
color: $color-gray-25;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
&__learn-more {
@include button-reset;
color: $color-gray-05;
@include light-theme {
color: $color-gray-90;
}
@include dark-theme {
color: $color-gray-05;
}
}
}
@ -242,8 +306,13 @@
&__stories-off-text {
flex: 1;
color: $color-gray-25;
@include font-subtitle;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
}
@ -271,7 +340,12 @@
&__members_help {
@include font-body-2;
color: $color-gray-25;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
&__remove_group {

View file

@ -167,16 +167,16 @@
}
}
&__icon {
@mixin StoryListItem__Icon($path) {
@include light-theme {
@include color-svg($path, $color-black);
}
@include dark-theme {
@include color-svg($path, $color-white);
}
@mixin StoryListItem__Icon($path) {
@include light-theme {
@include color-svg($path, $color-black);
}
@include dark-theme {
@include color-svg($path, $color-white);
}
}
&__icon {
&--chat {
@include StoryListItem__Icon('../images/icons/v3/open/open-compact.svg');
}
@ -203,9 +203,8 @@
margin-inline-start: 2px;
vertical-align: middle;
width: 16px;
@include color-svg(
'../images/icons/v3/chevron/chevron-right-compact-bold.svg',
$color-white
@include StoryListItem__Icon(
'../images/icons/v3/chevron/chevron-right-compact-bold.svg'
);
}
}

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 (

View file

@ -157,6 +157,7 @@ import {
callHistoryGroupSchema,
CallHistoryFilterStatus,
callHistoryDetailsSchema,
CallDirection,
} from '../types/CallDisposition';
type ConversationRow = Readonly<{
@ -3347,6 +3348,7 @@ async function getCallHistory(
const MISSED = sqlConstant(DirectCallStatus.Missed);
const DELETED = sqlConstant(DirectCallStatus.Deleted);
const INCOMING = sqlConstant(CallDirection.Incoming);
const FOUR_HOURS_IN_MS = sqlConstant(4 * 60 * 60 * 1000);
function getCallHistoryGroupDataSync(
@ -3405,7 +3407,10 @@ function getCallHistoryGroupDataSync(
const filterClause =
status === CallHistoryFilterStatus.All
? sqlFragment`status IS NOT ${DELETED}`
: sqlFragment`status IS ${MISSED} AND status IS NOT ${DELETED}`;
: sqlFragment`
direction IS ${INCOMING} AND
status IS ${MISSED} AND status IS NOT ${DELETED}
`;
const offsetLimit =
limit > 0 ? sqlFragment`LIMIT ${limit} OFFSET ${offset}` : sqlFragment``;

View file

@ -11,6 +11,8 @@ import type { ToastActionType } from './toast';
import { showToast } from './toast';
import { ToastType } from '../../types/Toast';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import * as log from '../../logging/log';
import * as Errors from '../../types/errors';
export type CallHistoryState = ReadonlyDeep<{
// This informs the app that underlying call history data has changed.
@ -19,19 +21,19 @@ export type CallHistoryState = ReadonlyDeep<{
}>;
const CALL_HISTORY_CACHE = 'callHistory/CACHE';
const CALL_HISTORY_CLEAR = 'callHistory/CLEAR';
const CALL_HISTORY_RESET = 'callHistory/RESET';
export type CallHistoryCache = ReadonlyDeep<{
type: typeof CALL_HISTORY_CACHE;
payload: CallHistoryDetails;
}>;
export type CallHistoryClear = ReadonlyDeep<{
type: typeof CALL_HISTORY_CLEAR;
export type CallHistoryReset = ReadonlyDeep<{
type: typeof CALL_HISTORY_RESET;
}>;
export type CallHistoryAction = ReadonlyDeep<
CallHistoryCache | CallHistoryClear
CallHistoryCache | CallHistoryReset
>;
export function getEmptyState(): CallHistoryState {
@ -52,12 +54,18 @@ function clearAllCallHistory(): ThunkAction<
void,
RootStateType,
unknown,
CallHistoryClear | ToastActionType
CallHistoryReset | ToastActionType
> {
return async dispatch => {
await clearCallHistoryDataAndSync();
dispatch({ type: CALL_HISTORY_CLEAR });
dispatch(showToast({ toastType: ToastType.CallHistoryCleared }));
try {
await clearCallHistoryDataAndSync();
dispatch(showToast({ toastType: ToastType.CallHistoryCleared }));
} catch (error) {
log.error('Error clearing call history', Errors.toLogFormat(error));
} finally {
// Just force a reset, even if the clear failed.
dispatch({ type: CALL_HISTORY_RESET });
}
};
}
@ -75,7 +83,7 @@ export function reducer(
action: CallHistoryAction
): CallHistoryState {
switch (action.type) {
case CALL_HISTORY_CLEAR:
case CALL_HISTORY_RESET:
return { ...state, edition: state.edition + 1, callHistoryByCallId: {} };
case CALL_HISTORY_CACHE:
return {

View file

@ -1328,6 +1328,11 @@ export function getPropsForCallHistory(
}: GetPropsForCallHistoryOptions
): CallingNotificationType {
const { callId } = message;
if (callId == null && 'callHistoryDetails' in message) {
log.error(
'getPropsForCallHistory: Found callHistoryDetails, but no callId'
);
}
strictAssert(callId != null, 'getPropsForCallHistory: Missing callId');
const callHistory = callHistorySelector(callId);
strictAssert(

View file

@ -15,7 +15,7 @@ import {
getMe,
} from '../selectors/conversations';
import { getDistributionListsWithMembers } from '../selectors/storyDistributionLists';
import { getIntl } from '../selectors/user';
import { getIntl, getTheme } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getHasStoryViewReceiptSetting } from '../selectors/items';
import { useGlobalModalActions } from '../ducks/globalModals';
@ -49,6 +49,7 @@ export function SmartStoriesSettingsModal(): JSX.Element | null {
const groupStories = useSelector(getGroupStories);
const getConversationByUuid = useSelector(getConversationByUuidSelector);
const theme = useSelector(getTheme);
return (
<StoriesSettingsModal
@ -70,6 +71,7 @@ export function SmartStoriesSettingsModal(): JSX.Element | null {
onViewersUpdated={updateStoryViewers}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
storyViewReceiptsEnabled={storyViewReceiptsEnabled}
theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
setStoriesDisabled={setStoriesDisabled}
/>

View file

@ -4,7 +4,7 @@
import React from 'react';
import { useSelector } from 'react-redux';
import type { LocalizerType } from '../../types/Util';
import { ThemeType, type LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer';
import { LinkPreviewSourceType } from '../../types/LinkPreview';
import { SmartCompositionTextArea } from './CompositionTextArea';
@ -140,6 +140,7 @@ export function SmartStoryCreator(): JSX.Element | null {
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
signalConnections={signalConnections}
skinTone={skinTone}
theme={ThemeType.dark}
toggleGroupsForStorySend={toggleGroupsForStorySend}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/>

View file

@ -2334,6 +2334,14 @@
"reasonCategory": "usageTrusted",
"updated": "2021-07-30T16:57:33.618Z"
},
{
"rule": "React-useRef",
"path": "ts/components/LeftPane.tsx",
"line": " const measureRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-08-09T21:48:42.602Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/components/LeftPaneSearchInput.tsx",
@ -2395,7 +2403,7 @@
"rule": "React-useRef",
"path": "ts/components/Modal.tsx",
"line": " const bodyRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2403,7 +2411,7 @@
"rule": "React-useRef",
"path": "ts/components/Modal.tsx",
"line": " const bodyInnerRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2556,7 +2564,7 @@
"rule": "React-useRef",
"path": "ts/components/TextAttachment.tsx",
"line": " const ref = useRef<HTMLDivElement>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2588,6 +2596,14 @@
"reasonCategory": "usageTrusted",
"updated": "2021-07-30T16:57:33.618Z"
},
{
"rule": "React-useRef",
"path": "ts/components/Tooltip.tsx",
"line": " const timeoutRef = useRef<NodeJS.Timeout | undefined>();",
"reasonCategory": "usageTrusted",
"updated": "2023-08-10T00:23:35.320Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.tsx",
@ -2806,7 +2822,7 @@
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const sizeRef = useRef<Size | null>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2814,7 +2830,7 @@
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const onSizeChangeRef = useRef<SizeChangeHandler | void>(onSizeChange);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2822,7 +2838,7 @@
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const ref = useRef<any>();",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2830,7 +2846,7 @@
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " * const scrollerRef = useRef()",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2838,7 +2854,7 @@
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " * const scrollerInnerRef = useRef()",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2846,7 +2862,7 @@
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const scrollRef = useRef<Scroll | null>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2854,7 +2870,7 @@
"rule": "React-useRef",
"path": "ts/hooks/useSizeObserver.tsx",
"line": " const onScrollChangeRef = useRef<ScrollChangeHandler>(onScrollChange);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-07-25T21:55:26.191Z",
"reasonDetail": "<optional>"
},
@ -2862,7 +2878,7 @@
"rule": "React-useRef",
"path": "ts/quill/formatting/menu.tsx",
"line": " const fadeOutTimerRef = React.useRef<NodeJS.Timeout | undefined>();",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-08-02T19:01:24.771Z",
"reasonDetail": "We need a persistent timer to know when to remove after fade-out"
},
@ -2870,7 +2886,7 @@
"rule": "React-useRef",
"path": "ts/quill/formatting/menu.tsx",
"line": " const hoverTimerRef = React.useRef<NodeJS.Timeout | undefined>();",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-08-02T19:01:24.771Z",
"reasonDetail": "We need a persistent timer to track long-hovers"
},
@ -2930,7 +2946,7 @@
"rule": "React-useRef",
"path": "ts/components/CallsList.tsx",
"line": " const infiniteLoaderRef = useRef<InfiniteLoader>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-08-02T00:21:37.858Z",
"reasonDetail": "<optional>"
},
@ -2938,7 +2954,7 @@
"rule": "React-useRef",
"path": "ts/components/CallsList.tsx",
"line": " const listRef = useRef<List>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"reasonCategory": "usageTrusted",
"updated": "2023-08-02T00:21:37.858Z",
"reasonDetail": "<optional>"
}