Switched ForwardMessageModal to use ListTile
This commit is contained in:
parent
257f5e1231
commit
d64e0b65c4
25 changed files with 528 additions and 239 deletions
|
@ -122,6 +122,8 @@ const rules = {
|
||||||
|
|
||||||
'react/display-name': 'error',
|
'react/display-name': 'error',
|
||||||
|
|
||||||
|
'react/jsx-pascal-case': ['error', {allowNamespace: true}],
|
||||||
|
|
||||||
// Allow returning values from promise executors for brevity.
|
// Allow returning values from promise executors for brevity.
|
||||||
'no-promise-executor-return': 'off',
|
'no-promise-executor-return': 'off',
|
||||||
|
|
||||||
|
|
|
@ -4405,6 +4405,13 @@ button.module-image__border-overlay:focus {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
|
||||||
|
// list tiles in choose-group-members and compose extend to the edge
|
||||||
|
.module-left-pane--mode-choose-group-members &,
|
||||||
|
.module-left-pane--mode-compose & {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&--width-narrow {
|
&--width-narrow {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
|
|
@ -64,4 +64,9 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-conversation-list {
|
||||||
|
// remove horizontal padding so ListTiles extend to the edges
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
max-height: 88px;
|
max-height: 88px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding-left: 12px;
|
padding: 4px 24px;
|
||||||
|
gap: 8px 12px;
|
||||||
|
|
||||||
.module-ContactPill {
|
.module-ContactPill {
|
||||||
margin: 4px 6px;
|
|
||||||
max-width: calc(
|
max-width: calc(
|
||||||
100% - 15px
|
100% - 15px
|
||||||
); // 6px for the right margin and 9px for the scrollbar
|
); // 6px for the right margin and 9px for the scrollbar
|
||||||
|
|
|
@ -22,6 +22,11 @@
|
||||||
color: $color-gray-05;
|
color: $color-gray-05;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-conversation-list {
|
||||||
|
// remove horizontal padding so ListTiles extend to the edges
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&--link-preview {
|
&--link-preview {
|
||||||
border-bottom: 1px solid $color-gray-15;
|
border-bottom: 1px solid $color-gray-15;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { noop, pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import type { MeasuredComponentProps } from 'react-measure';
|
import type { MeasuredComponentProps } from 'react-measure';
|
||||||
import Measure from 'react-measure';
|
import Measure from 'react-measure';
|
||||||
|
import type { ListRowProps } from 'react-virtualized';
|
||||||
|
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type {
|
import type {
|
||||||
|
@ -15,12 +16,16 @@ import type {
|
||||||
import { ToastType } from '../types/Toast';
|
import { ToastType } from '../types/Toast';
|
||||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import type { Row } from './ConversationList';
|
import type { GroupListItemConversationType } from './conversationList/GroupListItem';
|
||||||
import { ConversationList, RowType } from './ConversationList';
|
import {
|
||||||
import { DisabledReason } from './conversationList/GroupListItem';
|
DisabledReason,
|
||||||
|
GroupListItem,
|
||||||
|
} from './conversationList/GroupListItem';
|
||||||
import { Modal } from './Modal';
|
import { Modal } from './Modal';
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
import { useRestoreFocus } from '../hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../hooks/useRestoreFocus';
|
||||||
|
import { ListView } from './ListView';
|
||||||
|
import { ListTile } from './ListTile';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -47,7 +52,6 @@ export type Props = OwnProps & DispatchProps;
|
||||||
|
|
||||||
export function AddUserToAnotherGroupModal({
|
export function AddUserToAnotherGroupModal({
|
||||||
i18n,
|
i18n,
|
||||||
theme,
|
|
||||||
contact,
|
contact,
|
||||||
toggleAddUserToAnotherGroupModal,
|
toggleAddUserToAnotherGroupModal,
|
||||||
addMembersToGroup,
|
addMembersToGroup,
|
||||||
|
@ -108,7 +112,7 @@ export function AddUserToAnotherGroupModal({
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGetRow = React.useCallback(
|
const handleGetRow = React.useCallback(
|
||||||
(idx: number): Row | undefined => {
|
(idx: number): GroupListItemConversationType => {
|
||||||
const convo = filteredConversations[idx];
|
const convo = filteredConversations[idx];
|
||||||
|
|
||||||
// these are always populated in the case of a group
|
// these are always populated in the case of a group
|
||||||
|
@ -129,18 +133,36 @@ export function AddUserToAnotherGroupModal({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: RowType.SelectSingleGroup,
|
...pick(convo, 'id', 'avatarPath', 'title', 'unblurredAvatarPath'),
|
||||||
group: {
|
memberships,
|
||||||
...pick(convo, 'id', 'avatarPath', 'title', 'unblurredAvatarPath'),
|
membersCount,
|
||||||
memberships,
|
disabledReason,
|
||||||
membersCount,
|
|
||||||
disabledReason,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[filteredConversations, contact]
|
[filteredConversations, contact]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderGroupListItem = useCallback(
|
||||||
|
({ key, index, style }: ListRowProps) => {
|
||||||
|
const group = handleGetRow(index);
|
||||||
|
return (
|
||||||
|
<div key={key} style={style}>
|
||||||
|
<GroupListItem
|
||||||
|
i18n={i18n}
|
||||||
|
group={group}
|
||||||
|
onSelectGroup={setSelectedGroupId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[i18n, handleGetRow]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCalculateRowHeight = useCallback(
|
||||||
|
() => ListTile.heightCompact,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!selectedGroup && (
|
{!selectedGroup && (
|
||||||
|
@ -163,30 +185,30 @@ export function AddUserToAnotherGroupModal({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Measure bounds>
|
<Measure bounds>
|
||||||
{({ contentRect, measureRef }: MeasuredComponentProps) => (
|
{({ contentRect, measureRef }: MeasuredComponentProps) => {
|
||||||
<div
|
// Though `width` and `height` are required properties, we want to be
|
||||||
className="AddUserToAnotherGroupModal__list-wrapper"
|
// careful in case the caller sends bogus data. Notably, react-measure's
|
||||||
ref={measureRef}
|
// types seem to be inaccurate.
|
||||||
>
|
const { width = 100, height = 100 } = contentRect.bounds || {};
|
||||||
<ConversationList
|
if (!width || !height) {
|
||||||
dimensions={contentRect.bounds}
|
return null;
|
||||||
getPreferredBadge={() => undefined}
|
}
|
||||||
getRow={handleGetRow}
|
|
||||||
i18n={i18n}
|
return (
|
||||||
lookupConversationWithoutUuid={async _ => undefined}
|
<div
|
||||||
onClickArchiveButton={noop}
|
className="AddUserToAnotherGroupModal__list-wrapper"
|
||||||
onClickContactCheckbox={noop}
|
ref={measureRef}
|
||||||
onSelectConversation={setSelectedGroupId}
|
>
|
||||||
rowCount={filteredConversations.length}
|
<ListView
|
||||||
setIsFetchingUUID={noop}
|
width={width}
|
||||||
shouldRecomputeRowHeights={false}
|
height={height}
|
||||||
showChooseGroupMembers={noop}
|
rowCount={filteredConversations.length}
|
||||||
showConversation={noop}
|
calculateRowHeight={handleCalculateRowHeight}
|
||||||
showUserNotFoundModal={noop}
|
rowRenderer={renderGroupListItem}
|
||||||
theme={theme}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
);
|
||||||
)}
|
}}
|
||||||
</Measure>
|
</Measure>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -215,6 +215,9 @@ export function ConversationList({
|
||||||
case RowType.SearchResultsLoadingFakeHeader:
|
case RowType.SearchResultsLoadingFakeHeader:
|
||||||
return HEADER_ROW_HEIGHT;
|
return HEADER_ROW_HEIGHT;
|
||||||
case RowType.SelectSingleGroup:
|
case RowType.SelectSingleGroup:
|
||||||
|
case RowType.ContactCheckbox:
|
||||||
|
case RowType.Contact:
|
||||||
|
case RowType.CreateNewGroup:
|
||||||
return SELECT_ROW_HEIGHT;
|
return SELECT_ROW_HEIGHT;
|
||||||
default:
|
default:
|
||||||
return NORMAL_ROW_HEIGHT;
|
return NORMAL_ROW_HEIGHT;
|
||||||
|
|
|
@ -38,6 +38,7 @@ import {
|
||||||
shouldNeverBeCalled,
|
shouldNeverBeCalled,
|
||||||
asyncShouldNeverBeCalled,
|
asyncShouldNeverBeCalled,
|
||||||
} from '../util/shouldNeverBeCalled';
|
} from '../util/shouldNeverBeCalled';
|
||||||
|
import { Emojify } from './conversation/Emojify';
|
||||||
|
|
||||||
export type DataPropsType = {
|
export type DataPropsType = {
|
||||||
attachments?: ReadonlyArray<AttachmentType>;
|
attachments?: ReadonlyArray<AttachmentType>;
|
||||||
|
@ -408,8 +409,13 @@ export function ForwardMessageModal({
|
||||||
)}
|
)}
|
||||||
<div className="module-ForwardMessageModal__footer">
|
<div className="module-ForwardMessageModal__footer">
|
||||||
<div>
|
<div>
|
||||||
{Boolean(selectedContacts.length) &&
|
{Boolean(selectedContacts.length) && (
|
||||||
selectedContacts.map(contact => contact.title).join(', ')}
|
<Emojify
|
||||||
|
text={selectedContacts
|
||||||
|
.map(contact => contact.title)
|
||||||
|
.join(', ')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{isEditingMessage || !isMessageEditable ? (
|
{isEditingMessage || !isMessageEditable ? (
|
||||||
|
|
|
@ -23,14 +23,18 @@ import { setupI18n } from '../util/setupI18n';
|
||||||
import { DurationInSeconds, DAY } from '../util/durations';
|
import { DurationInSeconds, DAY } from '../util/durations';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
import { ThemeType } from '../types/Util';
|
import { ThemeType } from '../types/Util';
|
||||||
|
import {
|
||||||
|
getDefaultConversation,
|
||||||
|
getDefaultGroupListItem,
|
||||||
|
} from '../test-both/helpers/getDefaultConversation';
|
||||||
import { DialogType } from '../types/Dialogs';
|
import { DialogType } from '../types/Dialogs';
|
||||||
import { SocketStatus } from '../types/SocketStatus';
|
import { SocketStatus } from '../types/SocketStatus';
|
||||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
|
||||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
|
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
|
||||||
import {
|
import {
|
||||||
makeFakeLookupConversationWithoutUuid,
|
makeFakeLookupConversationWithoutUuid,
|
||||||
useUuidFetchState,
|
useUuidFetchState,
|
||||||
} from '../test-both/helpers/fakeLookupConversationWithoutUuid';
|
} from '../test-both/helpers/fakeLookupConversationWithoutUuid';
|
||||||
|
import type { GroupListItemConversationType } from './conversationList/GroupListItem';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -62,18 +66,14 @@ const defaultSearchProps = {
|
||||||
startSearchCounter: 0,
|
startSearchCounter: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultGroups: Array<ConversationType> = [
|
const defaultGroups: Array<GroupListItemConversationType> = [
|
||||||
getDefaultConversation({
|
getDefaultGroupListItem({
|
||||||
id: 'biking-group',
|
id: 'biking-group',
|
||||||
title: 'Mtn Biking Arizona 🚵☀️⛰',
|
title: 'Mtn Biking Arizona 🚵☀️⛰',
|
||||||
type: 'group',
|
|
||||||
sharedGroupNames: [],
|
|
||||||
}),
|
}),
|
||||||
getDefaultConversation({
|
getDefaultGroupListItem({
|
||||||
id: 'dance-group',
|
id: 'dance-group',
|
||||||
title: 'Are we dancers? 💃',
|
title: 'Are we dancers? 💃',
|
||||||
type: 'group',
|
|
||||||
sharedGroupNames: [],
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -605,7 +605,11 @@ export function LeftPane({
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-left-pane',
|
'module-left-pane',
|
||||||
isResizing && 'module-left-pane--is-resizing',
|
isResizing && 'module-left-pane--is-resizing',
|
||||||
`module-left-pane--width-${widthBreakpoint}`
|
`module-left-pane--width-${widthBreakpoint}`,
|
||||||
|
modeSpecificProps.mode === LeftPaneMode.ChooseGroupMembers &&
|
||||||
|
'module-left-pane--mode-choose-group-members',
|
||||||
|
modeSpecificProps.mode === LeftPaneMode.Compose &&
|
||||||
|
'module-left-pane--mode-compose'
|
||||||
)}
|
)}
|
||||||
style={{ width }}
|
style={{ width }}
|
||||||
>
|
>
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import uuid from 'uuid';
|
||||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||||
|
import { CircleCheckbox } from './CircleCheckbox';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
title: string | JSX.Element;
|
title: string | JSX.Element;
|
||||||
|
@ -23,6 +25,7 @@ export type Props = {
|
||||||
variant?: 'item' | 'panelrow';
|
variant?: 'item' | 'panelrow';
|
||||||
// defaults to div
|
// defaults to div
|
||||||
rootElement?: 'div' | 'button';
|
rootElement?: 'div' | 'button';
|
||||||
|
testId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClassName = getClassNamesFor('ListTile');
|
const getClassName = getClassNamesFor('ListTile');
|
||||||
|
@ -53,8 +56,17 @@ const getClassName = getClassNamesFor('ListTile');
|
||||||
* - panelrow: more horizontal padding, intended for information rows (usually not in
|
* - panelrow: more horizontal padding, intended for information rows (usually not in
|
||||||
* modals) that tend to occupy more horizontal space
|
* modals) that tend to occupy more horizontal space
|
||||||
*/
|
*/
|
||||||
export const ListTile = React.forwardRef<HTMLButtonElement, Props>(
|
export function ListTile(
|
||||||
function ListTile(
|
params: Props & React.RefAttributes<HTMLButtonElement>
|
||||||
|
): JSX.Element {
|
||||||
|
// forwardRef makes it impossible to add extra static fields to the function type so
|
||||||
|
// we have to create this inner implementation that can be wrapped with a non-arrow
|
||||||
|
// function. A bit weird, but looks fine at call-site.
|
||||||
|
return <ListTileImpl {...params} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListTileImpl = React.forwardRef<HTMLButtonElement, Props>(
|
||||||
|
function ListTileImpl(
|
||||||
{
|
{
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
@ -67,6 +79,7 @@ export const ListTile = React.forwardRef<HTMLButtonElement, Props>(
|
||||||
disabled = false,
|
disabled = false,
|
||||||
variant = 'item',
|
variant = 'item',
|
||||||
rootElement = 'div',
|
rootElement = 'div',
|
||||||
|
testId,
|
||||||
}: Props,
|
}: Props,
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
|
@ -81,6 +94,7 @@ export const ListTile = React.forwardRef<HTMLButtonElement, Props>(
|
||||||
onClick,
|
onClick,
|
||||||
'aria-disabled': disabled ? true : undefined,
|
'aria-disabled': disabled ? true : undefined,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
|
'data-testid': testId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const contents = (
|
const contents = (
|
||||||
|
@ -112,3 +126,52 @@ export const ListTile = React.forwardRef<HTMLButtonElement, Props>(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// although these heights are not required for ListTile (which sizes itself based on
|
||||||
|
// content), they are useful as constants for ListView.calculateRowHeight
|
||||||
|
|
||||||
|
/** Effective ListTile height for an avatar (leading) size 36 */
|
||||||
|
ListTile.heightFull = 64;
|
||||||
|
|
||||||
|
/** Effective ListTile height for an avatar (leading) size 48 */
|
||||||
|
ListTile.heightCompact = 52;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListTile with a trailing checkbox.
|
||||||
|
*
|
||||||
|
* It also wraps the ListTile with a <label> to get typical click-label-to-check behavior
|
||||||
|
*
|
||||||
|
* Same API except for:
|
||||||
|
* - no "trailing" param since it is populated by the checkbox
|
||||||
|
* - isChecked
|
||||||
|
*/
|
||||||
|
ListTile.checkbox = (
|
||||||
|
props: Omit<Props, 'trailing'> & { isChecked: boolean }
|
||||||
|
) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const htmlId: string = useMemo(() => uuid(), []);
|
||||||
|
|
||||||
|
const { onClick, disabled, isChecked, ...otherProps } = props;
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
htmlFor={htmlId}
|
||||||
|
// `onClick` is will double-fire if we're enabled. We want it to fire when we're
|
||||||
|
// disabled so we can show any "can't add contact" modals, etc. This won't
|
||||||
|
// work for keyboard users, though, because labels are not tabbable.
|
||||||
|
{...(disabled ? { onClick } : {})}
|
||||||
|
>
|
||||||
|
<ListTile
|
||||||
|
{...otherProps}
|
||||||
|
disabled={disabled}
|
||||||
|
trailing={
|
||||||
|
<CircleCheckbox
|
||||||
|
id={htmlId}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright 2018 Signal Messenger, LLC
|
// Copyright 2018 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable react/jsx-pascal-case */
|
||||||
|
|
||||||
import type { ReactNode, RefObject } from 'react';
|
import type { ReactNode, RefObject } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable local-rules/valid-i18n-keys */
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
@ -8,13 +10,14 @@ import React, {
|
||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { omit } from 'lodash';
|
import { noop, omit } from 'lodash';
|
||||||
import type { MeasuredComponentProps } from 'react-measure';
|
import type { MeasuredComponentProps } from 'react-measure';
|
||||||
import Measure from 'react-measure';
|
import Measure from 'react-measure';
|
||||||
|
import type { ListRowProps } from 'react-virtualized';
|
||||||
|
|
||||||
import type { LocalizerType, ThemeType } from '../../../../types/Util';
|
import type { LocalizerType, ThemeType } from '../../../../types/Util';
|
||||||
import { getUsernameFromSearch } from '../../../../types/Username';
|
import { getUsernameFromSearch } from '../../../../types/Username';
|
||||||
import { strictAssert } from '../../../../util/assert';
|
import { strictAssert, assertDev } from '../../../../util/assert';
|
||||||
import { refMerger } from '../../../../util/refMerger';
|
import { refMerger } from '../../../../util/refMerger';
|
||||||
import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
|
||||||
import { missingCaseError } from '../../../../util/missingCaseError';
|
import { missingCaseError } from '../../../../util/missingCaseError';
|
||||||
|
@ -36,11 +39,16 @@ import { ModalHost } from '../../../ModalHost';
|
||||||
import { ContactPills } from '../../../ContactPills';
|
import { ContactPills } from '../../../ContactPills';
|
||||||
import { ContactPill } from '../../../ContactPill';
|
import { ContactPill } from '../../../ContactPill';
|
||||||
import type { Row } from '../../../ConversationList';
|
import type { Row } from '../../../ConversationList';
|
||||||
import { ConversationList, RowType } from '../../../ConversationList';
|
import { RowType } from '../../../ConversationList';
|
||||||
import { ContactCheckboxDisabledReason } from '../../../conversationList/ContactCheckbox';
|
import {
|
||||||
|
ContactCheckbox,
|
||||||
|
ContactCheckboxDisabledReason,
|
||||||
|
} from '../../../conversationList/ContactCheckbox';
|
||||||
import { Button, ButtonVariant } from '../../../Button';
|
import { Button, ButtonVariant } from '../../../Button';
|
||||||
import { SearchInput } from '../../../SearchInput';
|
import { SearchInput } from '../../../SearchInput';
|
||||||
import { shouldNeverBeCalled } from '../../../../util/shouldNeverBeCalled';
|
import { ListView } from '../../../ListView';
|
||||||
|
import { UsernameCheckbox } from '../../../conversationList/UsernameCheckbox';
|
||||||
|
import { PhoneNumberCheckbox } from '../../../conversationList/PhoneNumberCheckbox';
|
||||||
|
|
||||||
export type StatePropsType = {
|
export type StatePropsType = {
|
||||||
regionCode: string | undefined;
|
regionCode: string | undefined;
|
||||||
|
@ -77,7 +85,6 @@ export function ChooseGroupMembersModal({
|
||||||
candidateContacts,
|
candidateContacts,
|
||||||
confirmAdds,
|
confirmAdds,
|
||||||
conversationIdsAlreadyInGroup,
|
conversationIdsAlreadyInGroup,
|
||||||
getPreferredBadge,
|
|
||||||
i18n,
|
i18n,
|
||||||
maxGroupSize,
|
maxGroupSize,
|
||||||
onClose,
|
onClose,
|
||||||
|
@ -278,6 +285,94 @@ export function ChooseGroupMembersModal({
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContactClick = (
|
||||||
|
conversationId: string,
|
||||||
|
disabledReason: undefined | ContactCheckboxDisabledReason
|
||||||
|
) => {
|
||||||
|
switch (disabledReason) {
|
||||||
|
case undefined:
|
||||||
|
toggleSelectedContact(conversationId);
|
||||||
|
break;
|
||||||
|
case ContactCheckboxDisabledReason.AlreadyAdded:
|
||||||
|
case ContactCheckboxDisabledReason.MaximumContactsSelected:
|
||||||
|
// These are no-ops.
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw missingCaseError(disabledReason);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderItem = ({ key, index, style }: ListRowProps) => {
|
||||||
|
const row = getRow(index);
|
||||||
|
|
||||||
|
let item;
|
||||||
|
switch (row?.type) {
|
||||||
|
case RowType.Header:
|
||||||
|
item = (
|
||||||
|
<div
|
||||||
|
className="module-conversation-list__item--header"
|
||||||
|
aria-label={i18n(row.i18nKey)}
|
||||||
|
>
|
||||||
|
{i18n(row.i18nKey)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case RowType.ContactCheckbox:
|
||||||
|
item = (
|
||||||
|
<ContactCheckbox
|
||||||
|
i18n={i18n}
|
||||||
|
theme={theme}
|
||||||
|
{...row.contact}
|
||||||
|
onClick={handleContactClick}
|
||||||
|
isChecked={row.isChecked}
|
||||||
|
badge={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case RowType.UsernameCheckbox:
|
||||||
|
item = (
|
||||||
|
<UsernameCheckbox
|
||||||
|
i18n={i18n}
|
||||||
|
theme={theme}
|
||||||
|
username={row.username}
|
||||||
|
isChecked={row.isChecked}
|
||||||
|
isFetching={row.isFetching}
|
||||||
|
toggleConversationInChooseMembers={conversationId =>
|
||||||
|
handleContactClick(conversationId, undefined)
|
||||||
|
}
|
||||||
|
showUserNotFoundModal={noop}
|
||||||
|
setIsFetchingUUID={setIsFetchingUUID}
|
||||||
|
lookupConversationWithoutUuid={() => Promise.resolve(undefined)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case RowType.PhoneNumberCheckbox:
|
||||||
|
item = (
|
||||||
|
<PhoneNumberCheckbox
|
||||||
|
phoneNumber={row.phoneNumber}
|
||||||
|
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
|
||||||
|
showUserNotFoundModal={showUserNotFoundModal}
|
||||||
|
setIsFetchingUUID={setIsFetchingUUID}
|
||||||
|
toggleConversationInChooseMembers={conversationId =>
|
||||||
|
handleContactClick(conversationId, undefined)
|
||||||
|
}
|
||||||
|
isChecked={row.isChecked}
|
||||||
|
isFetching={row.isFetching}
|
||||||
|
i18n={i18n}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} style={style}>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalHost
|
<ModalHost
|
||||||
modalName="AddGroupMembersModal.ChooseGroupMembersModal"
|
modalName="AddGroupMembersModal.ChooseGroupMembersModal"
|
||||||
|
@ -335,6 +430,14 @@ export function ChooseGroupMembersModal({
|
||||||
{rowCount ? (
|
{rowCount ? (
|
||||||
<Measure bounds>
|
<Measure bounds>
|
||||||
{({ contentRect, measureRef }: MeasuredComponentProps) => {
|
{({ contentRect, measureRef }: MeasuredComponentProps) => {
|
||||||
|
// Though `width` and `height` are required properties, we want to be
|
||||||
|
// careful in case the caller sends bogus data. Notably, react-measure's
|
||||||
|
// types seem to be inaccurate.
|
||||||
|
const { width = 100, height = 100 } = contentRect.bounds || {};
|
||||||
|
if (!width || !height) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// We disable this ESLint rule because we're capturing a bubbled keydown
|
// We disable this ESLint rule because we're capturing a bubbled keydown
|
||||||
// event. See [this note in the jsx-a11y docs][0].
|
// event. See [this note in the jsx-a11y docs][0].
|
||||||
//
|
//
|
||||||
|
@ -350,43 +453,25 @@ export function ChooseGroupMembersModal({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ConversationList
|
<ListView
|
||||||
dimensions={contentRect.bounds}
|
width={width}
|
||||||
getPreferredBadge={getPreferredBadge}
|
height={height}
|
||||||
getRow={getRow}
|
rowCount={rowCount}
|
||||||
i18n={i18n}
|
calculateRowHeight={index => {
|
||||||
onClickArchiveButton={shouldNeverBeCalled}
|
const row = getRow(index);
|
||||||
onClickContactCheckbox={(
|
if (!row) {
|
||||||
conversationId: string,
|
assertDev(false, `Expected a row at index ${index}`);
|
||||||
disabledReason: undefined | ContactCheckboxDisabledReason
|
return 52;
|
||||||
) => {
|
}
|
||||||
switch (disabledReason) {
|
|
||||||
case undefined:
|
switch (row.type) {
|
||||||
toggleSelectedContact(conversationId);
|
case RowType.Header:
|
||||||
break;
|
return 40;
|
||||||
case ContactCheckboxDisabledReason.AlreadyAdded:
|
|
||||||
case ContactCheckboxDisabledReason.MaximumContactsSelected:
|
|
||||||
// These are no-ops.
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(disabledReason);
|
return 52;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
lookupConversationWithoutUuid={
|
rowRenderer={renderItem}
|
||||||
lookupConversationWithoutUuid
|
|
||||||
}
|
|
||||||
showUserNotFoundModal={showUserNotFoundModal}
|
|
||||||
setIsFetchingUUID={setIsFetchingUUID}
|
|
||||||
showConversation={shouldNeverBeCalled}
|
|
||||||
onSelectConversation={shouldNeverBeCalled}
|
|
||||||
renderMessageSearchResult={() => {
|
|
||||||
shouldNeverBeCalled();
|
|
||||||
return <div />;
|
|
||||||
}}
|
|
||||||
rowCount={rowCount}
|
|
||||||
shouldRecomputeRowHeights={false}
|
|
||||||
showChooseGroupMembers={shouldNeverBeCalled}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -30,7 +30,7 @@ const MESSAGE_CLASS_NAME = `${CONTENT_CLASS_NAME}__message`;
|
||||||
export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`;
|
export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`;
|
||||||
const CHECKBOX_CONTAINER_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox--container`;
|
const CHECKBOX_CONTAINER_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox--container`;
|
||||||
const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`;
|
const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`;
|
||||||
const SPINNER_CLASS_NAME = `${BASE_CLASS_NAME}__spinner`;
|
export const SPINNER_CLASS_NAME = `${BASE_CLASS_NAME}__spinner`;
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { FunctionComponent, ReactNode } from 'react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import type { FunctionComponent } from 'react';
|
||||||
|
|
||||||
import {
|
import { HEADER_CONTACT_NAME_CLASS_NAME } from './BaseConversationListItem';
|
||||||
BaseConversationListItem,
|
|
||||||
HEADER_CONTACT_NAME_CLASS_NAME,
|
|
||||||
} from './BaseConversationListItem';
|
|
||||||
import type { ConversationType } from '../../state/ducks/conversations';
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
import type { BadgeType } from '../../badges/types';
|
import type { BadgeType } from '../../badges/types';
|
||||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
import { ContactName } from '../conversation/ContactName';
|
import { ContactName } from '../conversation/ContactName';
|
||||||
import { About } from '../conversation/About';
|
import { About } from '../conversation/About';
|
||||||
|
import { ListTile } from '../ListTile';
|
||||||
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
|
|
||||||
export enum ContactCheckboxDisabledReason {
|
export enum ContactCheckboxDisabledReason {
|
||||||
// We start the enum at 1 because the default starting value of 0 is falsy.
|
// We start the enum at 1 because the default starting value of 0 is falsy.
|
||||||
|
@ -61,7 +60,6 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
badge,
|
badge,
|
||||||
color,
|
color,
|
||||||
disabledReason,
|
disabledReason,
|
||||||
groupId,
|
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
isChecked,
|
isChecked,
|
||||||
|
@ -74,7 +72,6 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
uuid,
|
|
||||||
}) {
|
}) {
|
||||||
const disabled = Boolean(disabledReason);
|
const disabled = Boolean(disabledReason);
|
||||||
|
|
||||||
|
@ -86,13 +83,11 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
<ContactName module={HEADER_CONTACT_NAME_CLASS_NAME} title={title} />
|
<ContactName module={HEADER_CONTACT_NAME_CLASS_NAME} title={title} />
|
||||||
);
|
);
|
||||||
|
|
||||||
let messageText: ReactNode;
|
let messageText: undefined | string | JSX.Element;
|
||||||
if (disabledReason === ContactCheckboxDisabledReason.AlreadyAdded) {
|
if (disabledReason === ContactCheckboxDisabledReason.AlreadyAdded) {
|
||||||
messageText = i18n('alreadyAMember');
|
messageText = i18n('alreadyAMember');
|
||||||
} else if (about) {
|
} else if (about) {
|
||||||
messageText = <About className="" text={about} />;
|
messageText = <About className="" text={about} />;
|
||||||
} else {
|
|
||||||
messageText = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickItem = () => {
|
const onClickItem = () => {
|
||||||
|
@ -100,29 +95,33 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseConversationListItem
|
<ListTile.checkbox
|
||||||
acceptedMessageRequest={acceptedMessageRequest}
|
clickable
|
||||||
avatarPath={avatarPath}
|
|
||||||
badge={badge}
|
|
||||||
checked={isChecked}
|
|
||||||
color={color}
|
|
||||||
conversationType={type}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
groupId={groupId}
|
isChecked={isChecked}
|
||||||
headerName={headerName}
|
leading={
|
||||||
i18n={i18n}
|
<Avatar
|
||||||
id={id}
|
acceptedMessageRequest={acceptedMessageRequest}
|
||||||
isMe={isMe}
|
avatarPath={avatarPath}
|
||||||
isSelected={false}
|
color={color}
|
||||||
messageText={messageText}
|
conversationType={type}
|
||||||
|
noteToSelf={Boolean(isMe)}
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={isMe}
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
profileName={profileName}
|
||||||
|
title={title}
|
||||||
|
sharedGroupNames={sharedGroupNames}
|
||||||
|
size={AvatarSize.THIRTY_TWO}
|
||||||
|
unblurredAvatarPath={unblurredAvatarPath}
|
||||||
|
// appease the type checker.
|
||||||
|
{...(badge ? { badge, theme } : { badge: undefined })}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={headerName}
|
||||||
|
subtitle={isMe ? undefined : messageText}
|
||||||
|
subtitleMaxLines={1}
|
||||||
onClick={onClickItem}
|
onClick={onClickItem}
|
||||||
phoneNumber={phoneNumber}
|
|
||||||
profileName={profileName}
|
|
||||||
sharedGroupNames={sharedGroupNames}
|
|
||||||
theme={theme}
|
|
||||||
title={title}
|
|
||||||
unblurredAvatarPath={unblurredAvatarPath}
|
|
||||||
uuid={uuid}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,14 @@
|
||||||
import type { FunctionComponent } from 'react';
|
import type { FunctionComponent } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import { HEADER_CONTACT_NAME_CLASS_NAME } from './BaseConversationListItem';
|
||||||
BaseConversationListItem,
|
|
||||||
HEADER_CONTACT_NAME_CLASS_NAME,
|
|
||||||
} from './BaseConversationListItem';
|
|
||||||
import type { ConversationType } from '../../state/ducks/conversations';
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
import type { BadgeType } from '../../badges/types';
|
import type { BadgeType } from '../../badges/types';
|
||||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
import { ContactName } from '../conversation/ContactName';
|
import { ContactName } from '../conversation/ContactName';
|
||||||
import { About } from '../conversation/About';
|
import { About } from '../conversation/About';
|
||||||
|
import { ListTile } from '../ListTile';
|
||||||
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||||
|
|
||||||
export type ContactListItemConversationType = Pick<
|
export type ContactListItemConversationType = Pick<
|
||||||
|
@ -55,7 +54,6 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||||
avatarPath,
|
avatarPath,
|
||||||
badge,
|
badge,
|
||||||
color,
|
color,
|
||||||
groupId,
|
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
isMe,
|
isMe,
|
||||||
|
@ -82,30 +80,33 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
const messageText =
|
const messageText =
|
||||||
about && !isMe ? <About className="" text={about} /> : null;
|
about && !isMe ? <About className="" text={about} /> : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseConversationListItem
|
<ListTile
|
||||||
acceptedMessageRequest={acceptedMessageRequest}
|
leading={
|
||||||
avatarPath={avatarPath}
|
<Avatar
|
||||||
badge={badge}
|
acceptedMessageRequest={acceptedMessageRequest}
|
||||||
color={color}
|
avatarPath={avatarPath}
|
||||||
conversationType={type}
|
color={color}
|
||||||
groupId={groupId}
|
conversationType={type}
|
||||||
headerName={headerName}
|
noteToSelf={Boolean(isMe)}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={id}
|
isMe={isMe}
|
||||||
isMe={isMe}
|
phoneNumber={phoneNumber}
|
||||||
isSelected={false}
|
profileName={profileName}
|
||||||
messageText={messageText}
|
title={title}
|
||||||
|
sharedGroupNames={sharedGroupNames}
|
||||||
|
size={AvatarSize.THIRTY_TWO}
|
||||||
|
unblurredAvatarPath={unblurredAvatarPath}
|
||||||
|
// This is here to appease the type checker.
|
||||||
|
{...(badge ? { badge, theme } : { badge: undefined })}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={headerName}
|
||||||
|
subtitle={messageText}
|
||||||
|
subtitleMaxLines={1}
|
||||||
onClick={onClick ? () => onClick(id) : undefined}
|
onClick={onClick ? () => onClick(id) : undefined}
|
||||||
phoneNumber={phoneNumber}
|
|
||||||
profileName={profileName}
|
|
||||||
sharedGroupNames={sharedGroupNames}
|
|
||||||
theme={theme}
|
|
||||||
title={title}
|
|
||||||
unblurredAvatarPath={unblurredAvatarPath}
|
|
||||||
uuid={uuid}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
import type { FunctionComponent } from 'react';
|
import type { FunctionComponent } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import { ListTile } from '../ListTile';
|
||||||
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -17,17 +18,22 @@ export const CreateNewGroupButton: FunctionComponent<PropsType> = React.memo(
|
||||||
const title = i18n('createNewGroupButton');
|
const title = i18n('createNewGroupButton');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseConversationListItem
|
<ListTile
|
||||||
acceptedMessageRequest={false}
|
|
||||||
conversationType="group"
|
|
||||||
headerName={title}
|
|
||||||
i18n={i18n}
|
|
||||||
isMe={false}
|
|
||||||
isSelected={false}
|
|
||||||
onClick={onClick}
|
|
||||||
sharedGroupNames={[]}
|
|
||||||
testId="CreateNewGroupButton"
|
testId="CreateNewGroupButton"
|
||||||
|
leading={
|
||||||
|
<Avatar
|
||||||
|
acceptedMessageRequest={false}
|
||||||
|
conversationType="group"
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={false}
|
||||||
|
title={title}
|
||||||
|
sharedGroupNames={[]}
|
||||||
|
size={AvatarSize.THIRTY_TWO}
|
||||||
|
badge={undefined}
|
||||||
|
/>
|
||||||
|
}
|
||||||
title={title}
|
title={title}
|
||||||
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,9 @@ import React from 'react';
|
||||||
import type { ConversationType } from '../../state/ducks/conversations';
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import type { UUIDStringType } from '../../types/UUID';
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
import { AvatarSize } from '../Avatar';
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
import { Emojify } from '../conversation/Emojify';
|
||||||
|
import { ListTile } from '../ListTile';
|
||||||
|
|
||||||
export enum DisabledReason {
|
export enum DisabledReason {
|
||||||
AlreadyMember = 'already-member',
|
AlreadyMember = 'already-member',
|
||||||
|
@ -49,21 +50,25 @@ export function GroupListItem({
|
||||||
count: group.membersCount,
|
count: group.membersCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseConversationListItem
|
<ListTile
|
||||||
disabled={group.disabledReason !== undefined}
|
leading={
|
||||||
conversationType="group"
|
<Avatar
|
||||||
title={group.title}
|
acceptedMessageRequest
|
||||||
avatarSize={AvatarSize.THIRTY_TWO}
|
avatarPath={group.avatarPath}
|
||||||
avatarPath={group.avatarPath}
|
conversationType="group"
|
||||||
acceptedMessageRequest
|
i18n={i18n}
|
||||||
isMe={false}
|
isMe={false}
|
||||||
sharedGroupNames={[]}
|
title={group.title}
|
||||||
headerName={group.title}
|
sharedGroupNames={[]}
|
||||||
i18n={i18n}
|
size={AvatarSize.THIRTY_TWO}
|
||||||
isSelected={false}
|
badge={undefined}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={<Emojify text={group.title} />}
|
||||||
|
subtitle={<Emojify text={messageText} />}
|
||||||
onClick={() => onSelectGroup(group.id)}
|
onClick={() => onSelectGroup(group.id)}
|
||||||
messageText={messageText}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,15 @@ import React, { useState } from 'react';
|
||||||
|
|
||||||
import { ButtonVariant } from '../Button';
|
import { ButtonVariant } from '../Button';
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
import { SPINNER_CLASS_NAME } from './BaseConversationListItem';
|
||||||
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
||||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
import { AvatarColors } from '../../types/Colors';
|
import { AvatarColors } from '../../types/Colors';
|
||||||
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
|
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
|
||||||
|
import { ListTile } from '../ListTile';
|
||||||
|
import { Emojify } from '../conversation/Emojify';
|
||||||
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
|
import { Spinner } from '../Spinner';
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
phoneNumber: ParsedE164Type;
|
phoneNumber: ParsedE164Type;
|
||||||
|
@ -31,7 +35,6 @@ export const PhoneNumberCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
isChecked,
|
isChecked,
|
||||||
isFetching,
|
isFetching,
|
||||||
theme,
|
|
||||||
i18n,
|
i18n,
|
||||||
lookupConversationWithoutUuid,
|
lookupConversationWithoutUuid,
|
||||||
showUserNotFoundModal,
|
showUserNotFoundModal,
|
||||||
|
@ -88,24 +91,46 @@ export const PhoneNumberCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const avatar = (
|
||||||
|
<Avatar
|
||||||
|
acceptedMessageRequest={false}
|
||||||
|
color={AvatarColors[0]}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={false}
|
||||||
|
phoneNumber={phoneNumber.userInput}
|
||||||
|
title={phoneNumber.userInput}
|
||||||
|
sharedGroupNames={[]}
|
||||||
|
size={AvatarSize.THIRTY_TWO}
|
||||||
|
badge={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const title = <Emojify text={phoneNumber.userInput} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseConversationListItem
|
{isFetching ? (
|
||||||
acceptedMessageRequest={false}
|
<ListTile
|
||||||
checked={isChecked}
|
leading={avatar}
|
||||||
color={AvatarColors[0]}
|
title={title}
|
||||||
conversationType="direct"
|
trailing={
|
||||||
headerName={phoneNumber.userInput}
|
<Spinner
|
||||||
i18n={i18n}
|
size="20px"
|
||||||
isMe={false}
|
svgSize="small"
|
||||||
isSelected={false}
|
moduleClassName={SPINNER_CLASS_NAME}
|
||||||
onClick={onClickItem}
|
direction="on-progress-dialog"
|
||||||
phoneNumber={phoneNumber.userInput}
|
/>
|
||||||
shouldShowSpinner={isFetching}
|
}
|
||||||
theme={theme}
|
/>
|
||||||
sharedGroupNames={[]}
|
) : (
|
||||||
title={phoneNumber.userInput}
|
<ListTile.checkbox
|
||||||
/>
|
isChecked={isChecked}
|
||||||
|
onClick={onClickItem}
|
||||||
|
leading={avatar}
|
||||||
|
title={<Emojify text={phoneNumber.userInput} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{modal}
|
{modal}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { FunctionComponent } from 'react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import type { FunctionComponent } from 'react';
|
||||||
|
|
||||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
|
||||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
import { AvatarColors } from '../../types/Colors';
|
import { AvatarColors } from '../../types/Colors';
|
||||||
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
|
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
|
||||||
|
import { ListTile } from '../ListTile';
|
||||||
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
|
import { Spinner } from '../Spinner';
|
||||||
|
import { SPINNER_CLASS_NAME } from './BaseConversationListItem';
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -28,7 +31,6 @@ export const UsernameCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
username,
|
username,
|
||||||
isChecked,
|
isChecked,
|
||||||
isFetching,
|
isFetching,
|
||||||
theme,
|
|
||||||
i18n,
|
i18n,
|
||||||
lookupConversationWithoutUuid,
|
lookupConversationWithoutUuid,
|
||||||
showUserNotFoundModal,
|
showUserNotFoundModal,
|
||||||
|
@ -62,22 +64,41 @@ export const UsernameCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
|
|
||||||
const title = i18n('at-username', { username });
|
const title = i18n('at-username', { username });
|
||||||
|
|
||||||
return (
|
const avatar = (
|
||||||
<BaseConversationListItem
|
<Avatar
|
||||||
acceptedMessageRequest={false}
|
acceptedMessageRequest={false}
|
||||||
checked={isChecked}
|
|
||||||
color={AvatarColors[0]}
|
color={AvatarColors[0]}
|
||||||
conversationType="direct"
|
conversationType="direct"
|
||||||
headerName={title}
|
searchResult
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isMe={false}
|
isMe={false}
|
||||||
isSelected={false}
|
|
||||||
isUsernameSearchResult
|
|
||||||
onClick={onClickItem}
|
|
||||||
shouldShowSpinner={isFetching}
|
|
||||||
theme={theme}
|
|
||||||
sharedGroupNames={[]}
|
|
||||||
title={title}
|
title={title}
|
||||||
|
sharedGroupNames={[]}
|
||||||
|
size={AvatarSize.THIRTY_TWO}
|
||||||
|
badge={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return isFetching ? (
|
||||||
|
<ListTile
|
||||||
|
leading={avatar}
|
||||||
|
title={title}
|
||||||
|
trailing={
|
||||||
|
<Spinner
|
||||||
|
size="20px"
|
||||||
|
svgSize="small"
|
||||||
|
moduleClassName={SPINNER_CLASS_NAME}
|
||||||
|
direction="on-progress-dialog"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ListTile.checkbox
|
||||||
|
leading={avatar}
|
||||||
|
title={title}
|
||||||
|
isChecked={isChecked}
|
||||||
|
onClick={onClickItem}
|
||||||
|
clickable
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { LeftPaneHelper } from './LeftPaneHelper';
|
||||||
import type { Row } from '../ConversationList';
|
import type { Row } from '../ConversationList';
|
||||||
import { RowType } from '../ConversationList';
|
import { RowType } from '../ConversationList';
|
||||||
import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
|
import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
|
||||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
|
||||||
import { SearchInput } from '../SearchInput';
|
import { SearchInput } from '../SearchInput';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
||||||
|
@ -20,10 +19,11 @@ import {
|
||||||
isFetchingByUsername,
|
isFetchingByUsername,
|
||||||
isFetchingByE164,
|
isFetchingByE164,
|
||||||
} from '../../util/uuidFetchState';
|
} from '../../util/uuidFetchState';
|
||||||
|
import type { GroupListItemConversationType } from '../conversationList/GroupListItem';
|
||||||
|
|
||||||
export type LeftPaneComposePropsType = {
|
export type LeftPaneComposePropsType = {
|
||||||
composeContacts: ReadonlyArray<ContactListItemConversationType>;
|
composeContacts: ReadonlyArray<ContactListItemConversationType>;
|
||||||
composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
composeGroups: ReadonlyArray<GroupListItemConversationType>;
|
||||||
|
|
||||||
regionCode: string | undefined;
|
regionCode: string | undefined;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
@ -39,7 +39,7 @@ enum TopButton {
|
||||||
export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsType> {
|
export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsType> {
|
||||||
private readonly composeContacts: ReadonlyArray<ContactListItemConversationType>;
|
private readonly composeContacts: ReadonlyArray<ContactListItemConversationType>;
|
||||||
|
|
||||||
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
private readonly composeGroups: ReadonlyArray<GroupListItemConversationType>;
|
||||||
|
|
||||||
private readonly uuidFetchState: UUIDFetchStateType;
|
private readonly uuidFetchState: UUIDFetchStateType;
|
||||||
|
|
||||||
|
@ -224,8 +224,8 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
const group = this.composeGroups[virtualRowIndex];
|
const group = this.composeGroups[virtualRowIndex];
|
||||||
if (group) {
|
if (group) {
|
||||||
return {
|
return {
|
||||||
type: RowType.Conversation,
|
type: RowType.SelectSingleGroup,
|
||||||
conversation: group,
|
group,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -613,8 +613,25 @@ export const getFilteredComposeGroups = createSelector(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
groups: ReadonlyArray<ConversationType>,
|
groups: ReadonlyArray<ConversationType>,
|
||||||
regionCode: string | undefined
|
regionCode: string | undefined
|
||||||
): Array<ConversationType> => {
|
): Array<
|
||||||
return filterAndSortConversationsByRecent(groups, searchTerm, regionCode);
|
ConversationType & {
|
||||||
|
membersCount: number;
|
||||||
|
disabledReason: undefined;
|
||||||
|
memberships: ReadonlyArray<unknown>;
|
||||||
|
}
|
||||||
|
> => {
|
||||||
|
return filterAndSortConversationsByRecent(
|
||||||
|
groups,
|
||||||
|
searchTerm,
|
||||||
|
regionCode
|
||||||
|
).map(group => ({
|
||||||
|
...group,
|
||||||
|
// we don't disable groups when composing, already filtered
|
||||||
|
disabledReason: undefined,
|
||||||
|
// should always be populated for a group
|
||||||
|
membersCount: group.membersCount ?? 0,
|
||||||
|
memberships: group.memberships ?? [],
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
import casual from 'casual';
|
import casual from 'casual';
|
||||||
import { sample } from 'lodash';
|
import { sample } from 'lodash';
|
||||||
import type { ConversationType } from '../../state/ducks/conversations';
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
import { UUID } from '../../types/UUID';
|
|
||||||
import type { UUIDStringType } from '../../types/UUID';
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
|
import { UUID } from '../../types/UUID';
|
||||||
|
import type { GroupListItemConversationType } from '../../components/conversationList/GroupListItem';
|
||||||
import { getRandomColor } from './getRandomColor';
|
import { getRandomColor } from './getRandomColor';
|
||||||
import { ConversationColors } from '../../types/Colors';
|
import { ConversationColors } from '../../types/Colors';
|
||||||
import { StorySendMode } from '../../types/Stories';
|
import { StorySendMode } from '../../types/Stories';
|
||||||
|
@ -46,6 +47,18 @@ export function getDefaultConversation(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDefaultGroupListItem(
|
||||||
|
overrideProps: Partial<GroupListItemConversationType> = {}
|
||||||
|
): GroupListItemConversationType {
|
||||||
|
return {
|
||||||
|
...getDefaultGroup(),
|
||||||
|
disabledReason: undefined,
|
||||||
|
membersCount: 24,
|
||||||
|
memberships: [],
|
||||||
|
...overrideProps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getDefaultGroup(
|
export function getDefaultGroup(
|
||||||
overrideProps: Partial<ConversationType> = {}
|
overrideProps: Partial<ConversationType> = {}
|
||||||
): ConversationType {
|
): ConversationType {
|
||||||
|
|
|
@ -113,9 +113,7 @@ describe('pnp/send gv2 invite', function needsName() {
|
||||||
.locator('.module-left-pane__compose-search-form__input')
|
.locator('.module-left-pane__compose-search-form__input')
|
||||||
.fill('ACI');
|
.fill('ACI');
|
||||||
|
|
||||||
await leftPane
|
await leftPane.locator('.ListTile >> "ACI Contact"').click();
|
||||||
.locator(`[data-testid="${aciContact.toContact().uuid}"]`)
|
|
||||||
.click();
|
|
||||||
|
|
||||||
debug('inviting PNI member');
|
debug('inviting PNI member');
|
||||||
|
|
||||||
|
@ -123,11 +121,7 @@ describe('pnp/send gv2 invite', function needsName() {
|
||||||
.locator('.module-left-pane__compose-search-form__input')
|
.locator('.module-left-pane__compose-search-form__input')
|
||||||
.fill('PNI');
|
.fill('PNI');
|
||||||
|
|
||||||
await leftPane
|
await leftPane.locator('.ListTile >> "PNI Contact"').click();
|
||||||
.locator(
|
|
||||||
`[data-testid="${pniContact.device.getUUIDByKind(UUIDKind.PNI)}"]`
|
|
||||||
)
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await leftPane
|
await leftPane
|
||||||
.locator('.module-left-pane__footer button >> "Next"')
|
.locator('.module-left-pane__footer button >> "Next"')
|
||||||
|
|
|
@ -5,7 +5,10 @@ import { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { RowType } from '../../../components/ConversationList';
|
import { RowType } from '../../../components/ConversationList';
|
||||||
import { FindDirection } from '../../../components/leftPane/LeftPaneHelper';
|
import { FindDirection } from '../../../components/leftPane/LeftPaneHelper';
|
||||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
import {
|
||||||
|
getDefaultConversation,
|
||||||
|
getDefaultGroupListItem,
|
||||||
|
} from '../../../test-both/helpers/getDefaultConversation';
|
||||||
|
|
||||||
import { LeftPaneComposeHelper } from '../../../components/leftPane/LeftPaneComposeHelper';
|
import { LeftPaneComposeHelper } from '../../../components/leftPane/LeftPaneComposeHelper';
|
||||||
|
|
||||||
|
@ -69,7 +72,7 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneComposeHelper({
|
new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
isUsernamesEnabled: true,
|
isUsernamesEnabled: true,
|
||||||
|
@ -83,7 +86,7 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneComposeHelper({
|
new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'someone',
|
searchTerm: 'someone',
|
||||||
isUsernamesEnabled: true,
|
isUsernamesEnabled: true,
|
||||||
|
@ -97,7 +100,7 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneComposeHelper({
|
new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'someone',
|
searchTerm: 'someone',
|
||||||
isUsernamesEnabled: false,
|
isUsernamesEnabled: false,
|
||||||
|
@ -133,7 +136,7 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneComposeHelper({
|
new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
composeGroups: [getDefaultConversation()],
|
composeGroups: [getDefaultGroupListItem()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foobar',
|
searchTerm: 'foobar',
|
||||||
isUsernamesEnabled: true,
|
isUsernamesEnabled: true,
|
||||||
|
@ -240,8 +243,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
];
|
];
|
||||||
const composeGroups = [
|
const composeGroups = [
|
||||||
getDefaultConversation(),
|
getDefaultGroupListItem(),
|
||||||
getDefaultConversation(),
|
getDefaultGroupListItem(),
|
||||||
];
|
];
|
||||||
const helper = new LeftPaneComposeHelper({
|
const helper = new LeftPaneComposeHelper({
|
||||||
composeContacts,
|
composeContacts,
|
||||||
|
@ -255,6 +258,7 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
assert.deepEqual(helper.getRow(0), {
|
assert.deepEqual(helper.getRow(0), {
|
||||||
type: RowType.CreateNewGroup,
|
type: RowType.CreateNewGroup,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(helper.getRow(1), {
|
assert.deepEqual(helper.getRow(1), {
|
||||||
type: RowType.Header,
|
type: RowType.Header,
|
||||||
i18nKey: 'contactsHeader',
|
i18nKey: 'contactsHeader',
|
||||||
|
@ -272,12 +276,12 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
i18nKey: 'groupsHeader',
|
i18nKey: 'groupsHeader',
|
||||||
});
|
});
|
||||||
assert.deepEqual(helper.getRow(5), {
|
assert.deepEqual(helper.getRow(5), {
|
||||||
type: RowType.Conversation,
|
type: RowType.SelectSingleGroup,
|
||||||
conversation: composeGroups[0],
|
group: composeGroups[0],
|
||||||
});
|
});
|
||||||
assert.deepEqual(helper.getRow(6), {
|
assert.deepEqual(helper.getRow(6), {
|
||||||
type: RowType.Conversation,
|
type: RowType.SelectSingleGroup,
|
||||||
conversation: composeGroups[1],
|
group: composeGroups[1],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -571,7 +575,7 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
helperContacts.shouldRecomputeRowHeights({
|
helperContacts.shouldRecomputeRowHeights({
|
||||||
composeContacts: [],
|
composeContacts: [],
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
isUsernamesEnabled: true,
|
isUsernamesEnabled: true,
|
||||||
|
@ -581,7 +585,7 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
|
|
||||||
const helperGroups = new LeftPaneComposeHelper({
|
const helperGroups = new LeftPaneComposeHelper({
|
||||||
composeContacts: [],
|
composeContacts: [],
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
isUsernamesEnabled: true,
|
isUsernamesEnabled: true,
|
||||||
|
@ -603,7 +607,7 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
it('should be true if the headers are in different row indices as before', () => {
|
it('should be true if the headers are in different row indices as before', () => {
|
||||||
const helperContacts = new LeftPaneComposeHelper({
|
const helperContacts = new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
composeGroups: [getDefaultConversation()],
|
composeGroups: [getDefaultGroupListItem()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'soup',
|
searchTerm: 'soup',
|
||||||
isUsernamesEnabled: true,
|
isUsernamesEnabled: true,
|
||||||
|
@ -613,7 +617,7 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
helperContacts.shouldRecomputeRowHeights({
|
helperContacts.shouldRecomputeRowHeights({
|
||||||
composeContacts: [getDefaultConversation()],
|
composeContacts: [getDefaultConversation()],
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'soup',
|
searchTerm: 'soup',
|
||||||
isUsernamesEnabled: true,
|
isUsernamesEnabled: true,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue