Display user badges
This commit is contained in:
parent
927c22ef73
commit
f647c4e053
95 changed files with 2891 additions and 424 deletions
|
@ -4,7 +4,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { Intl } from './Intl';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||
import { Modal } from './Modal';
|
||||
import { ConversationListItem } from './conversationList/ConversationListItem';
|
||||
|
||||
|
@ -12,12 +12,14 @@ type PropsType = {
|
|||
groupAdmins: Array<ConversationType>;
|
||||
i18n: LocalizerType;
|
||||
openConversation: (conversationId: string) => unknown;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
export const AnnouncementsOnlyGroupBanner = ({
|
||||
groupAdmins,
|
||||
i18n,
|
||||
openConversation,
|
||||
theme,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [isShowingAdmins, setIsShowingAdmins] = useState(false);
|
||||
|
||||
|
@ -40,6 +42,7 @@ export const AnnouncementsOnlyGroupBanner = ({
|
|||
lastMessage={undefined}
|
||||
lastUpdated={undefined}
|
||||
typingContact={undefined}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
|
|
|
@ -14,6 +14,8 @@ import { setupI18n } from '../util/setupI18n';
|
|||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
|
||||
import { getFakeBadge } from '../test-both/helpers/getFakeBadge';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -37,6 +39,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
? overrideProps.acceptedMessageRequest
|
||||
: true,
|
||||
avatarPath: text('avatarPath', overrideProps.avatarPath || ''),
|
||||
badge: overrideProps.badge,
|
||||
blur: overrideProps.blur,
|
||||
color: select('color', colorMap, overrideProps.color || AvatarColors[0]),
|
||||
conversationType: select(
|
||||
|
@ -66,6 +69,27 @@ story.add('Avatar', () => {
|
|||
return sizes.map(size => <Avatar key={size} {...props} size={size} />);
|
||||
});
|
||||
|
||||
story.add('With badge', () => {
|
||||
const Wrapper = () => {
|
||||
const theme = React.useContext(StorybookThemeContext);
|
||||
const props = createProps({
|
||||
avatarPath: '/fixtures/kitten-3-64-64.jpg',
|
||||
badge: getFakeBadge(),
|
||||
theme,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{sizes.map(size => (
|
||||
<Avatar key={size} {...props} size={size} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return <Wrapper />;
|
||||
});
|
||||
|
||||
story.add('Wide image', () => {
|
||||
const props = createProps({
|
||||
avatarPath: '/fixtures/wide.jpg',
|
||||
|
|
|
@ -15,10 +15,14 @@ import { Spinner } from './Spinner';
|
|||
|
||||
import { getInitials } from '../util/getInitials';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import type { BadgeType } from '../badges/types';
|
||||
import * as log from '../logging/log';
|
||||
import { assert } from '../util/assert';
|
||||
import { shouldBlurAvatar } from '../util/shouldBlurAvatar';
|
||||
import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath';
|
||||
import { BadgeImageTheme } from '../badges/BadgeImageTheme';
|
||||
|
||||
export enum AvatarBlur {
|
||||
NoBlur,
|
||||
|
@ -40,6 +44,7 @@ export enum AvatarSize {
|
|||
|
||||
export type Props = {
|
||||
avatarPath?: string;
|
||||
badge?: BadgeType;
|
||||
blur?: AvatarBlur;
|
||||
color?: AvatarColorType;
|
||||
loading?: boolean;
|
||||
|
@ -53,6 +58,7 @@ export type Props = {
|
|||
profileName?: string;
|
||||
sharedGroupNames: Array<string>;
|
||||
size: AvatarSize;
|
||||
theme?: ThemeType;
|
||||
title: string;
|
||||
unblurredAvatarPath?: string;
|
||||
|
||||
|
@ -72,6 +78,7 @@ const getDefaultBlur = (
|
|||
export const Avatar: FunctionComponent<Props> = ({
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
badge,
|
||||
className,
|
||||
color = 'A200',
|
||||
conversationType,
|
||||
|
@ -83,6 +90,7 @@ export const Avatar: FunctionComponent<Props> = ({
|
|||
onClick,
|
||||
sharedGroupNames,
|
||||
size,
|
||||
theme,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
blur = getDefaultBlur({
|
||||
|
@ -203,6 +211,33 @@ export const Avatar: FunctionComponent<Props> = ({
|
|||
contents = <div className={contentsClassName}>{contentsChildren}</div>;
|
||||
}
|
||||
|
||||
let badgeNode: ReactNode;
|
||||
if (badge && theme && !isMe) {
|
||||
const badgeSize = Math.ceil(size * 0.425);
|
||||
const badgeTheme =
|
||||
theme === ThemeType.light ? BadgeImageTheme.Light : BadgeImageTheme.Dark;
|
||||
const badgeImagePath = getBadgeImageFileLocalPath(
|
||||
badge,
|
||||
badgeSize,
|
||||
badgeTheme
|
||||
);
|
||||
if (badgeImagePath) {
|
||||
badgeNode = (
|
||||
<img
|
||||
alt={badge.name}
|
||||
className="module-Avatar__badge"
|
||||
src={badgeImagePath}
|
||||
style={{
|
||||
width: badgeSize,
|
||||
height: badgeSize,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (badge && !theme) {
|
||||
log.error('<Avatar> requires a theme if a badge is provided');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label={i18n('contactAvatarAlt', [title])}
|
||||
|
@ -219,6 +254,7 @@ export const Avatar: FunctionComponent<Props> = ({
|
|||
ref={innerRef}
|
||||
>
|
||||
{contents}
|
||||
{badgeNode}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
40
ts/components/BadgeCarouselIndex.tsx
Normal file
40
ts/components/BadgeCarouselIndex.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { times } from 'lodash';
|
||||
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
||||
export function BadgeCarouselIndex({
|
||||
currentIndex,
|
||||
totalCount,
|
||||
}: Readonly<{
|
||||
currentIndex: number;
|
||||
totalCount: number;
|
||||
}>): JSX.Element | null {
|
||||
strictAssert(totalCount >= 1, 'Expected 1 or more items');
|
||||
strictAssert(
|
||||
currentIndex < totalCount,
|
||||
'Expected current index to be in range'
|
||||
);
|
||||
|
||||
if (totalCount < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div aria-hidden className="BadgeCarouselIndex">
|
||||
{times(totalCount, index => (
|
||||
<div
|
||||
key={index}
|
||||
className={classNames(
|
||||
'BadgeCarouselIndex__dot',
|
||||
currentIndex === index && 'BadgeCarouselIndex__dot--selected'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
24
ts/components/BadgeDescription.stories.tsx
Normal file
24
ts/components/BadgeDescription.stories.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { BadgeDescription } from './BadgeDescription';
|
||||
|
||||
const story = storiesOf('Components/BadgeDescription', module);
|
||||
|
||||
story.add('Normal name', () => (
|
||||
<BadgeDescription
|
||||
template="{short_name} is here! Hello, {short_name}! {short_name}, I think you're great. This is not replaced: {not_replaced}"
|
||||
firstName="Alice"
|
||||
title="Should not be seen"
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Name with RTL overrides', () => (
|
||||
<BadgeDescription
|
||||
template="Hello, {short_name}! {short_name}, I think you're great."
|
||||
title={'Flip-\u202eflop'}
|
||||
/>
|
||||
));
|
42
ts/components/BadgeDescription.tsx
Normal file
42
ts/components/BadgeDescription.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactChild, ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
|
||||
export function BadgeDescription({
|
||||
firstName,
|
||||
template,
|
||||
title,
|
||||
}: Readonly<{
|
||||
firstName?: string;
|
||||
template: string;
|
||||
title: string;
|
||||
}>): ReactElement {
|
||||
const result: Array<ReactChild> = [];
|
||||
|
||||
let lastIndex = 0;
|
||||
|
||||
const matches = template.matchAll(/\{short_name\}/g);
|
||||
for (const match of matches) {
|
||||
const matchIndex = match.index || 0;
|
||||
|
||||
result.push(template.slice(lastIndex, matchIndex));
|
||||
|
||||
result.push(
|
||||
<ContactName
|
||||
key={matchIndex}
|
||||
firstName={firstName}
|
||||
title={title}
|
||||
preferFirstName
|
||||
/>
|
||||
);
|
||||
|
||||
lastIndex = matchIndex + 12;
|
||||
}
|
||||
|
||||
result.push(template.slice(lastIndex));
|
||||
|
||||
return <>{result}</>;
|
||||
}
|
97
ts/components/BadgeDialog.stories.tsx
Normal file
97
ts/components/BadgeDialog.stories.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { getFakeBadge, getFakeBadges } from '../test-both/helpers/getFakeBadge';
|
||||
import { repeat, zipObject } from '../util/iterables';
|
||||
import { BadgeImageTheme } from '../badges/BadgeImageTheme';
|
||||
import { BadgeDialog } from './BadgeDialog';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf('Components/BadgeDialog', module);
|
||||
|
||||
const defaultProps: ComponentProps<typeof BadgeDialog> = {
|
||||
badges: getFakeBadges(3),
|
||||
firstName: 'Alice',
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
title: 'Alice Levine',
|
||||
};
|
||||
|
||||
story.add('No badges (closed immediately)', () => (
|
||||
<BadgeDialog {...defaultProps} badges={[]} />
|
||||
));
|
||||
|
||||
story.add('One badge', () => (
|
||||
<BadgeDialog {...defaultProps} badges={getFakeBadges(1)} />
|
||||
));
|
||||
|
||||
story.add('Badge with no image (should be impossible)', () => (
|
||||
<BadgeDialog
|
||||
{...defaultProps}
|
||||
badges={[
|
||||
{
|
||||
...getFakeBadge(),
|
||||
images: [],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Badge with pending image', () => (
|
||||
<BadgeDialog
|
||||
{...defaultProps}
|
||||
badges={[
|
||||
{
|
||||
...getFakeBadge(),
|
||||
images: Array(4).fill(
|
||||
zipObject(
|
||||
Object.values(BadgeImageTheme),
|
||||
repeat({ url: 'https://example.com/ignored.svg' })
|
||||
)
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Badge with only one, low-detail image', () => (
|
||||
<BadgeDialog
|
||||
{...defaultProps}
|
||||
badges={[
|
||||
{
|
||||
...getFakeBadge(),
|
||||
images: [
|
||||
zipObject(
|
||||
Object.values(BadgeImageTheme),
|
||||
repeat({
|
||||
localPath: '/fixtures/orange-heart.svg',
|
||||
url: 'https://example.com/ignored.svg',
|
||||
})
|
||||
),
|
||||
...Array(3).fill(
|
||||
zipObject(
|
||||
Object.values(BadgeImageTheme),
|
||||
repeat({ url: 'https://example.com/ignored.svg' })
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Five badges', () => (
|
||||
<BadgeDialog {...defaultProps} badges={getFakeBadges(5)} />
|
||||
));
|
||||
|
||||
story.add('Many badges', () => (
|
||||
<BadgeDialog {...defaultProps} badges={getFakeBadges(50)} />
|
||||
));
|
109
ts/components/BadgeDialog.tsx
Normal file
109
ts/components/BadgeDialog.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { strictAssert } from '../util/assert';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { BadgeType } from '../badges/types';
|
||||
import { Modal } from './Modal';
|
||||
import { BadgeDescription } from './BadgeDescription';
|
||||
import { BadgeImage } from './BadgeImage';
|
||||
import { BadgeCarouselIndex } from './BadgeCarouselIndex';
|
||||
|
||||
type PropsType = Readonly<{
|
||||
badges: ReadonlyArray<BadgeType>;
|
||||
firstName?: string;
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
export function BadgeDialog(props: PropsType): null | JSX.Element {
|
||||
const { badges, onClose } = props;
|
||||
|
||||
const hasBadges = badges.length > 0;
|
||||
useEffect(() => {
|
||||
if (!hasBadges) {
|
||||
onClose();
|
||||
}
|
||||
}, [hasBadges, onClose]);
|
||||
|
||||
return hasBadges ? <BadgeDialogWithBadges {...props} /> : null;
|
||||
}
|
||||
|
||||
function BadgeDialogWithBadges({
|
||||
badges,
|
||||
firstName,
|
||||
i18n,
|
||||
onClose,
|
||||
title,
|
||||
}: PropsType): JSX.Element {
|
||||
const firstBadge = badges[0];
|
||||
strictAssert(
|
||||
firstBadge,
|
||||
'<BadgeDialogWithBadges> got an empty array of badges'
|
||||
);
|
||||
|
||||
const [currentBadgeId, setCurrentBadgeId] = useState(firstBadge.id);
|
||||
|
||||
let currentBadge: BadgeType;
|
||||
let currentBadgeIndex: number = badges.findIndex(
|
||||
b => b.id === currentBadgeId
|
||||
);
|
||||
if (currentBadgeIndex === -1) {
|
||||
currentBadgeIndex = 0;
|
||||
currentBadge = firstBadge;
|
||||
} else {
|
||||
currentBadge = badges[currentBadgeIndex];
|
||||
}
|
||||
|
||||
const setCurrentBadgeIndex = (index: number): void => {
|
||||
const newBadge = badges[index];
|
||||
strictAssert(newBadge, '<BadgeDialog> tried to select a nonexistent badge');
|
||||
setCurrentBadgeId(newBadge.id);
|
||||
};
|
||||
|
||||
const navigate = (change: number): void => {
|
||||
setCurrentBadgeIndex(currentBadgeIndex + change);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hasXButton
|
||||
moduleClassName="BadgeDialog"
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('previous')}
|
||||
className="BadgeDialog__nav BadgeDialog__nav--previous"
|
||||
disabled={currentBadgeIndex === 0}
|
||||
onClick={() => navigate(-1)}
|
||||
type="button"
|
||||
/>
|
||||
<div className="BadgeDialog__main">
|
||||
<BadgeImage badge={currentBadge} size={200} />
|
||||
<div className="BadgeDialog__name">{currentBadge.name}</div>
|
||||
<div className="BadgeDialog__description">
|
||||
<BadgeDescription
|
||||
firstName={firstName}
|
||||
template={currentBadge.descriptionTemplate}
|
||||
title={title}
|
||||
/>
|
||||
</div>
|
||||
<BadgeCarouselIndex
|
||||
currentIndex={currentBadgeIndex}
|
||||
totalCount={badges.length}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-label={i18n('next')}
|
||||
className="BadgeDialog__nav BadgeDialog__nav--next"
|
||||
disabled={currentBadgeIndex === badges.length - 1}
|
||||
onClick={() => navigate(1)}
|
||||
type="button"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
48
ts/components/BadgeImage.tsx
Normal file
48
ts/components/BadgeImage.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { BadgeType } from '../badges/types';
|
||||
import { Spinner } from './Spinner';
|
||||
import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath';
|
||||
import { BadgeImageTheme } from '../badges/BadgeImageTheme';
|
||||
|
||||
export function BadgeImage({
|
||||
badge,
|
||||
size,
|
||||
}: Readonly<{
|
||||
badge: BadgeType;
|
||||
size: number;
|
||||
}>): JSX.Element {
|
||||
const { name } = badge;
|
||||
|
||||
const imagePath = getBadgeImageFileLocalPath(
|
||||
badge,
|
||||
size,
|
||||
BadgeImageTheme.Transparent
|
||||
);
|
||||
|
||||
if (!imagePath) {
|
||||
return (
|
||||
<Spinner
|
||||
ariaLabel={name}
|
||||
moduleClassName="BadgeImage BadgeImage__loading"
|
||||
size={`${size}px`}
|
||||
svgSize="normal"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
alt={name}
|
||||
className="BadgeImage"
|
||||
src={imagePath}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -15,6 +15,7 @@ import enMessages from '../../_locales/en/messages.json';
|
|||
|
||||
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
|
||||
import { landscapeGreenUrl } from '../storybook/Fixtures';
|
||||
import { ThemeType } from '../types/Util';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -31,6 +32,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
onSendMessage: action('onSendMessage'),
|
||||
processAttachments: action('processAttachments'),
|
||||
removeAttachment: action('removeAttachment'),
|
||||
theme: ThemeType.light,
|
||||
|
||||
// AttachmentList
|
||||
draftAttachments: overrideProps.draftAttachments || [],
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
|||
BodyRangeType,
|
||||
BodyRangesType,
|
||||
LocalizerType,
|
||||
ThemeType,
|
||||
} from '../types/Util';
|
||||
import type { ErrorDialogAudioRecorderType } from '../state/ducks/audioRecorder';
|
||||
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
|
||||
|
@ -117,6 +118,7 @@ export type OwnProps = Readonly<{
|
|||
setQuotedMessage(message: undefined): unknown;
|
||||
shouldSendHighQualityAttachments: boolean;
|
||||
startRecording: () => unknown;
|
||||
theme: ThemeType;
|
||||
}>;
|
||||
|
||||
export type Props = Pick<
|
||||
|
@ -162,6 +164,7 @@ export const CompositionArea = ({
|
|||
onSendMessage,
|
||||
processAttachments,
|
||||
removeAttachment,
|
||||
theme,
|
||||
|
||||
// AttachmentList
|
||||
draftAttachments,
|
||||
|
@ -542,6 +545,7 @@ export const CompositionArea = ({
|
|||
groupAdmins={groupAdmins}
|
||||
i18n={i18n}
|
||||
openConversation={openConversation}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { times, omit } from 'lodash';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean, date, select, text } from '@storybook/addon-knobs';
|
||||
|
||||
import type { PropsType, Row } from './ConversationList';
|
||||
import type { Row } from './ConversationList';
|
||||
import { ConversationList, RowType } from './ConversationList';
|
||||
import { MessageSearchResult } from './conversationList/MessageSearchResult';
|
||||
import type { PropsData as ConversationListItemPropsType } from './conversationList/ConversationListItem';
|
||||
|
@ -17,6 +17,7 @@ import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbo
|
|||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -46,52 +47,58 @@ const defaultConversations: Array<ConversationListItemPropsType> = [
|
|||
getDefaultConversation(),
|
||||
];
|
||||
|
||||
const createProps = (rows: ReadonlyArray<Row>): PropsType => ({
|
||||
dimensions: {
|
||||
width: 300,
|
||||
height: 350,
|
||||
},
|
||||
rowCount: rows.length,
|
||||
getRow: (index: number) => rows[index],
|
||||
shouldRecomputeRowHeights: false,
|
||||
i18n,
|
||||
onSelectConversation: action('onSelectConversation'),
|
||||
onClickArchiveButton: action('onClickArchiveButton'),
|
||||
onClickContactCheckbox: action('onClickContactCheckbox'),
|
||||
renderMessageSearchResult: (id: string) => (
|
||||
<MessageSearchResult
|
||||
body="Lorem ipsum wow"
|
||||
bodyRanges={[]}
|
||||
conversationId="marc-convo"
|
||||
from={defaultConversations[0]}
|
||||
const Wrapper = ({
|
||||
rows,
|
||||
scrollable,
|
||||
}: Readonly<{ rows: ReadonlyArray<Row>; scrollable?: boolean }>) => {
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return (
|
||||
<ConversationList
|
||||
dimensions={{
|
||||
width: 300,
|
||||
height: 350,
|
||||
}}
|
||||
rowCount={rows.length}
|
||||
getRow={(index: number) => rows[index]}
|
||||
shouldRecomputeRowHeights={false}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
openConversationInternal={action('openConversationInternal')}
|
||||
sentAt={1587358800000}
|
||||
snippet="Lorem <<left>>ipsum<<right>> wow"
|
||||
to={defaultConversations[1]}
|
||||
onSelectConversation={action('onSelectConversation')}
|
||||
onClickArchiveButton={action('onClickArchiveButton')}
|
||||
onClickContactCheckbox={action('onClickContactCheckbox')}
|
||||
renderMessageSearchResult={(id: string) => (
|
||||
<MessageSearchResult
|
||||
body="Lorem ipsum wow"
|
||||
bodyRanges={[]}
|
||||
conversationId="marc-convo"
|
||||
from={defaultConversations[0]}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
openConversationInternal={action('openConversationInternal')}
|
||||
sentAt={1587358800000}
|
||||
snippet="Lorem <<left>>ipsum<<right>> wow"
|
||||
to={defaultConversations[1]}
|
||||
/>
|
||||
)}
|
||||
scrollable={scrollable}
|
||||
showChooseGroupMembers={action('showChooseGroupMembers')}
|
||||
startNewConversationFromPhoneNumber={action(
|
||||
'startNewConversationFromPhoneNumber'
|
||||
)}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
showChooseGroupMembers: action('showChooseGroupMembers'),
|
||||
startNewConversationFromPhoneNumber: action(
|
||||
'startNewConversationFromPhoneNumber'
|
||||
),
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
story.add('Archive button', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
{
|
||||
type: RowType.ArchiveButton,
|
||||
archivedConversationsCount: 123,
|
||||
},
|
||||
])}
|
||||
<Wrapper
|
||||
rows={[{ type: RowType.ArchiveButton, archivedConversationsCount: 123 }]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Contact: note to self', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.Contact,
|
||||
contact: {
|
||||
|
@ -100,35 +107,30 @@ story.add('Contact: note to self', () => (
|
|||
about: '🤠 should be ignored',
|
||||
},
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Contact: direct', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
{
|
||||
type: RowType.Contact,
|
||||
contact: defaultConversations[0],
|
||||
},
|
||||
])}
|
||||
<Wrapper
|
||||
rows={[{ type: RowType.Contact, contact: defaultConversations[0] }]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Contact: direct with short about', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.Contact,
|
||||
contact: { ...defaultConversations[0], about: '🤠 yee haw' },
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Contact: direct with long about', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.Contact,
|
||||
contact: {
|
||||
|
@ -137,24 +139,24 @@ story.add('Contact: direct with long about', () => (
|
|||
'🤠 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue.',
|
||||
},
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Contact: group', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.Contact,
|
||||
contact: { ...defaultConversations[0], type: 'group' },
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Contact checkboxes', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.ContactCheckbox,
|
||||
contact: defaultConversations[0],
|
||||
|
@ -173,13 +175,13 @@ story.add('Contact checkboxes', () => (
|
|||
},
|
||||
isChecked: true,
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Contact checkboxes: disabled', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.ContactCheckbox,
|
||||
contact: defaultConversations[0],
|
||||
|
@ -204,7 +206,7 @@ story.add('Contact checkboxes: disabled', () => (
|
|||
isChecked: true,
|
||||
disabledReason: ContactCheckboxDisabledReason.AlreadyAdded,
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
|
@ -219,6 +221,7 @@ story.add('Contact checkboxes: disabled', () => (
|
|||
? overrideProps.acceptedMessageRequest
|
||||
: true
|
||||
),
|
||||
badges: [],
|
||||
isMe: boolean('isMe', overrideProps.isMe || false),
|
||||
avatarPath: text('avatarPath', overrideProps.avatarPath || ''),
|
||||
id: overrideProps.id || '',
|
||||
|
@ -246,13 +249,13 @@ story.add('Contact checkboxes: disabled', () => (
|
|||
const renderConversation = (
|
||||
overrideProps: Partial<ConversationListItemPropsType> = {}
|
||||
) => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation(overrideProps),
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -278,15 +281,13 @@ story.add('Contact checkboxes: disabled', () => (
|
|||
);
|
||||
|
||||
story.add('Conversations: Message Statuses', () => (
|
||||
<ConversationList
|
||||
{...createProps(
|
||||
MessageStatuses.map(status => ({
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation({
|
||||
lastMessage: { text: status, status, deletedForEveryone: false },
|
||||
}),
|
||||
}))
|
||||
)}
|
||||
<Wrapper
|
||||
rows={MessageStatuses.map(status => ({
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation({
|
||||
lastMessage: { text: status, status, deletedForEveryone: false },
|
||||
}),
|
||||
}))}
|
||||
/>
|
||||
));
|
||||
|
||||
|
@ -324,20 +325,18 @@ story.add('Contact checkboxes: disabled', () => (
|
|||
);
|
||||
|
||||
story.add('Conversations: unread count', () => (
|
||||
<ConversationList
|
||||
{...createProps(
|
||||
[4, 10, 34, 250].map(unreadCount => ({
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation({
|
||||
lastMessage: {
|
||||
text: 'Hey there!',
|
||||
status: 'delivered',
|
||||
deletedForEveryone: false,
|
||||
},
|
||||
unreadCount,
|
||||
}),
|
||||
}))
|
||||
)}
|
||||
<Wrapper
|
||||
rows={[4, 10, 34, 250].map(unreadCount => ({
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation({
|
||||
lastMessage: {
|
||||
text: 'Hey there!',
|
||||
status: 'delivered',
|
||||
deletedForEveryone: false,
|
||||
},
|
||||
unreadCount,
|
||||
}),
|
||||
}))}
|
||||
/>
|
||||
));
|
||||
|
||||
|
@ -396,19 +395,17 @@ Line 4, well.`,
|
|||
];
|
||||
|
||||
return (
|
||||
<ConversationList
|
||||
{...createProps(
|
||||
messages.map(messageText => ({
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation({
|
||||
lastMessage: {
|
||||
text: messageText,
|
||||
status: 'read',
|
||||
deletedForEveryone: false,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
)}
|
||||
<Wrapper
|
||||
rows={messages.map(messageText => ({
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation({
|
||||
lastMessage: {
|
||||
text: messageText,
|
||||
status: 'read',
|
||||
deletedForEveryone: false,
|
||||
},
|
||||
}),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -422,20 +419,18 @@ Line 4, well.`,
|
|||
];
|
||||
|
||||
return (
|
||||
<ConversationList
|
||||
{...createProps(
|
||||
pairs.map(([lastUpdated, messageText]) => ({
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation({
|
||||
lastUpdated,
|
||||
lastMessage: {
|
||||
text: messageText,
|
||||
status: 'read',
|
||||
deletedForEveryone: false,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
)}
|
||||
<Wrapper
|
||||
rows={pairs.map(([lastUpdated, messageText]) => ({
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation({
|
||||
lastUpdated,
|
||||
lastMessage: {
|
||||
text: messageText,
|
||||
status: 'read',
|
||||
deletedForEveryone: false,
|
||||
},
|
||||
}),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -446,7 +441,7 @@ Line 4, well.`,
|
|||
conversation: omit(createConversation(), 'lastUpdated'),
|
||||
};
|
||||
|
||||
return <ConversationList {...createProps([row])} />;
|
||||
return <Wrapper rows={[row]} />;
|
||||
});
|
||||
|
||||
story.add('Conversation: Missing Message', () => {
|
||||
|
@ -455,7 +450,7 @@ Line 4, well.`,
|
|||
conversation: omit(createConversation(), 'lastMessage'),
|
||||
};
|
||||
|
||||
return <ConversationList {...createProps([row])} />;
|
||||
return <Wrapper rows={[row]} />;
|
||||
});
|
||||
|
||||
story.add('Conversation: Missing Text', () =>
|
||||
|
@ -488,8 +483,8 @@ Line 4, well.`,
|
|||
}
|
||||
|
||||
story.add('Headers', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.Header,
|
||||
i18nKey: 'conversationsHeader',
|
||||
|
@ -498,36 +493,36 @@ story.add('Headers', () => (
|
|||
type: RowType.Header,
|
||||
i18nKey: 'messagesHeader',
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Start new conversation', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: '+12345559876',
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Search results loading skeleton', () => (
|
||||
<ConversationList
|
||||
<Wrapper
|
||||
scrollable={false}
|
||||
{...createProps([
|
||||
rows={[
|
||||
{ type: RowType.SearchResultsLoadingFakeHeader },
|
||||
...times(99, () => ({
|
||||
type: RowType.SearchResultsLoadingFakeRow as const,
|
||||
})),
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Kitchen sink', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: '+12345559876',
|
||||
|
@ -552,6 +547,6 @@ story.add('Kitchen sink', () => (
|
|||
type: RowType.ArchiveButton,
|
||||
archivedConversationsCount: 123,
|
||||
},
|
||||
])}
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -8,11 +8,13 @@ import { List } from 'react-virtualized';
|
|||
import classNames from 'classnames';
|
||||
import { get, pick } from 'lodash';
|
||||
|
||||
import { getOwn } from '../util/getOwn';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { assert } from '../util/assert';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||
import { ScrollBehavior } from '../types/Util';
|
||||
import { getConversationListWidthBreakpoint } from './_util';
|
||||
import type { BadgeType } from '../badges/types';
|
||||
|
||||
import type { PropsData as ConversationListItemPropsType } from './conversationList/ConversationListItem';
|
||||
import { ConversationListItem } from './conversationList/ConversationListItem';
|
||||
|
@ -105,6 +107,7 @@ export type Row =
|
|||
| StartNewConversationRowType;
|
||||
|
||||
export type PropsType = {
|
||||
badgesById?: Record<string, BadgeType>;
|
||||
dimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
|
@ -120,6 +123,7 @@ export type PropsType = {
|
|||
scrollable?: boolean;
|
||||
|
||||
i18n: LocalizerType;
|
||||
theme: ThemeType;
|
||||
|
||||
onClickArchiveButton: () => void;
|
||||
onClickContactCheckbox: (
|
||||
|
@ -136,6 +140,7 @@ const NORMAL_ROW_HEIGHT = 76;
|
|||
const HEADER_ROW_HEIGHT = 40;
|
||||
|
||||
export const ConversationList: React.FC<PropsType> = ({
|
||||
badgesById,
|
||||
dimensions,
|
||||
getRow,
|
||||
i18n,
|
||||
|
@ -150,6 +155,7 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
shouldRecomputeRowHeights,
|
||||
showChooseGroupMembers,
|
||||
startNewConversationFromPhoneNumber,
|
||||
theme,
|
||||
}) => {
|
||||
const listRef = useRef<null | List>(null);
|
||||
|
||||
|
@ -235,6 +241,7 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
const itemProps = pick(row.conversation, [
|
||||
'acceptedMessageRequest',
|
||||
'avatarPath',
|
||||
'badges',
|
||||
'color',
|
||||
'draftPreview',
|
||||
'id',
|
||||
|
@ -255,7 +262,12 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
'unblurredAvatarPath',
|
||||
'unreadCount',
|
||||
]);
|
||||
const { title, unreadCount, lastMessage } = itemProps;
|
||||
const { badges, title, unreadCount, lastMessage } = itemProps;
|
||||
|
||||
let badge: undefined | BadgeType;
|
||||
if (badgesById && badges[0]) {
|
||||
badge = getOwn(badgesById, badges[0].id);
|
||||
}
|
||||
|
||||
result = (
|
||||
<div
|
||||
|
@ -270,8 +282,10 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
<ConversationListItem
|
||||
{...itemProps}
|
||||
key={key}
|
||||
badge={badge}
|
||||
onClick={onSelectConversation}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -326,6 +340,7 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
);
|
||||
},
|
||||
[
|
||||
badgesById,
|
||||
getRow,
|
||||
i18n,
|
||||
onClickArchiveButton,
|
||||
|
@ -334,6 +349,7 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
renderMessageSearchResult,
|
||||
showChooseGroupMembers,
|
||||
startNewConversationFromPhoneNumber,
|
||||
theme,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { ForwardMessageModal } from './ForwardMessageModal';
|
|||
import { IMAGE_JPEG, VIDEO_MP4, stringToMIMEType } from '../types/MIME';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
|
||||
|
||||
const createAttachment = (
|
||||
props: Partial<AttachmentType> = {}
|
||||
|
@ -39,7 +40,7 @@ const candidateConversations = Array.from(Array(100), () =>
|
|||
getDefaultConversation()
|
||||
);
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
attachments: overrideProps.attachments,
|
||||
candidateConversations,
|
||||
doForwardMessage: action('doForwardMessage'),
|
||||
|
@ -55,24 +56,25 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
recentEmojis: [],
|
||||
removeLinkPreview: action('removeLinkPreview'),
|
||||
skinTone: 0,
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
});
|
||||
|
||||
story.add('Modal', () => {
|
||||
return <ForwardMessageModal {...createProps()} />;
|
||||
return <ForwardMessageModal {...useProps()} />;
|
||||
});
|
||||
|
||||
story.add('with text', () => {
|
||||
return <ForwardMessageModal {...createProps({ messageBody: 'sup' })} />;
|
||||
return <ForwardMessageModal {...useProps({ messageBody: 'sup' })} />;
|
||||
});
|
||||
|
||||
story.add('a sticker', () => {
|
||||
return <ForwardMessageModal {...createProps({ isSticker: true })} />;
|
||||
return <ForwardMessageModal {...useProps({ isSticker: true })} />;
|
||||
});
|
||||
|
||||
story.add('link preview', () => {
|
||||
return (
|
||||
<ForwardMessageModal
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
linkPreview: {
|
||||
description: LONG_DESCRIPTION,
|
||||
date: Date.now(),
|
||||
|
@ -94,7 +96,7 @@ story.add('link preview', () => {
|
|||
story.add('media attachments', () => {
|
||||
return (
|
||||
<ForwardMessageModal
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
attachments: [
|
||||
createAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
|
@ -122,7 +124,7 @@ story.add('media attachments', () => {
|
|||
|
||||
story.add('announcement only groups non-admin', () => (
|
||||
<ForwardMessageModal
|
||||
{...createProps()}
|
||||
{...useProps()}
|
||||
candidateConversations={[
|
||||
getDefaultConversation({
|
||||
announcementsOnly: true,
|
||||
|
|
|
@ -29,7 +29,7 @@ import type { Props as EmojiButtonProps } from './emoji/EmojiButton';
|
|||
import { EmojiButton } from './emoji/EmojiButton';
|
||||
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import type { BodyRangeType, LocalizerType } from '../types/Util';
|
||||
import type { BodyRangeType, LocalizerType, ThemeType } from '../types/Util';
|
||||
import { ModalHost } from './ModalHost';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||
|
@ -57,6 +57,7 @@ export type DataPropsType = {
|
|||
caretLocation?: number
|
||||
) => unknown;
|
||||
onTextTooLong: () => void;
|
||||
theme: ThemeType;
|
||||
} & Pick<EmojiButtonProps, 'recentEmojis' | 'skinTone'>;
|
||||
|
||||
type ActionPropsType = Pick<
|
||||
|
@ -86,6 +87,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
recentEmojis,
|
||||
removeLinkPreview,
|
||||
skinTone,
|
||||
theme,
|
||||
}) => {
|
||||
const inputRef = useRef<null | HTMLInputElement>(null);
|
||||
const inputApiRef = React.useRef<InputApi | undefined>();
|
||||
|
@ -412,6 +414,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
startNewConversationFromPhoneNumber={
|
||||
shouldNeverBeCalled
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -15,6 +15,7 @@ import { MessageSearchResult } from './conversationList/MessageSearchResult';
|
|||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -79,7 +80,8 @@ const defaultModeSpecificProps = {
|
|||
|
||||
const emptySearchResultsGroup = { isLoading: false, results: [] };
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
badgesById: {},
|
||||
cantAddContactToGroup: action('cantAddContactToGroup'),
|
||||
canResizeLeftPane: true,
|
||||
clearGroupCreationError: action('clearGroupCreationError'),
|
||||
|
@ -146,6 +148,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
),
|
||||
startSearch: action('startSearch'),
|
||||
startSettingGroupMetadata: action('startSettingGroupMetadata'),
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
toggleComposeEditingAvatar: action('toggleComposeEditingAvatar'),
|
||||
toggleConversationInChooseMembers: action(
|
||||
'toggleConversationInChooseMembers'
|
||||
|
@ -159,7 +162,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
|
||||
story.add('Inbox: no conversations', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
pinnedConversations: [],
|
||||
|
@ -174,7 +177,7 @@ story.add('Inbox: no conversations', () => (
|
|||
|
||||
story.add('Inbox: only pinned conversations', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
pinnedConversations,
|
||||
|
@ -189,7 +192,7 @@ story.add('Inbox: only pinned conversations', () => (
|
|||
|
||||
story.add('Inbox: only non-pinned conversations', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
pinnedConversations: [],
|
||||
|
@ -204,7 +207,7 @@ story.add('Inbox: only non-pinned conversations', () => (
|
|||
|
||||
story.add('Inbox: only archived conversations', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
pinnedConversations: [],
|
||||
|
@ -219,7 +222,7 @@ story.add('Inbox: only archived conversations', () => (
|
|||
|
||||
story.add('Inbox: pinned and archived conversations', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
pinnedConversations,
|
||||
|
@ -234,7 +237,7 @@ story.add('Inbox: pinned and archived conversations', () => (
|
|||
|
||||
story.add('Inbox: non-pinned and archived conversations', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
pinnedConversations: [],
|
||||
|
@ -249,7 +252,7 @@ story.add('Inbox: non-pinned and archived conversations', () => (
|
|||
|
||||
story.add('Inbox: pinned and non-pinned conversations', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
pinnedConversations,
|
||||
|
@ -263,14 +266,14 @@ story.add('Inbox: pinned and non-pinned conversations', () => (
|
|||
));
|
||||
|
||||
story.add('Inbox: pinned, non-pinned, and archived conversations', () => (
|
||||
<LeftPane {...createProps()} />
|
||||
<LeftPane {...useProps()} />
|
||||
));
|
||||
|
||||
// Search stories
|
||||
|
||||
story.add('Search: no results when searching everywhere', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Search,
|
||||
conversationResults: emptySearchResultsGroup,
|
||||
|
@ -285,7 +288,7 @@ story.add('Search: no results when searching everywhere', () => (
|
|||
|
||||
story.add('Search: no results when searching everywhere (SMS)', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Search,
|
||||
conversationResults: emptySearchResultsGroup,
|
||||
|
@ -300,7 +303,7 @@ story.add('Search: no results when searching everywhere (SMS)', () => (
|
|||
|
||||
story.add('Search: no results when searching in a conversation', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Search,
|
||||
conversationResults: emptySearchResultsGroup,
|
||||
|
@ -316,7 +319,7 @@ story.add('Search: no results when searching in a conversation', () => (
|
|||
|
||||
story.add('Search: all results loading', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Search,
|
||||
conversationResults: { isLoading: true },
|
||||
|
@ -331,7 +334,7 @@ story.add('Search: all results loading', () => (
|
|||
|
||||
story.add('Search: some results loading', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Search,
|
||||
conversationResults: {
|
||||
|
@ -349,7 +352,7 @@ story.add('Search: some results loading', () => (
|
|||
|
||||
story.add('Search: has conversations and contacts, but not messages', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Search,
|
||||
conversationResults: {
|
||||
|
@ -367,7 +370,7 @@ story.add('Search: has conversations and contacts, but not messages', () => (
|
|||
|
||||
story.add('Search: all results', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Search,
|
||||
conversationResults: {
|
||||
|
@ -393,7 +396,7 @@ story.add('Search: all results', () => (
|
|||
|
||||
story.add('Archive: no archived conversations', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Archive,
|
||||
archivedConversations: [],
|
||||
|
@ -406,7 +409,7 @@ story.add('Archive: no archived conversations', () => (
|
|||
|
||||
story.add('Archive: archived conversations', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Archive,
|
||||
archivedConversations: defaultConversations,
|
||||
|
@ -419,7 +422,7 @@ story.add('Archive: archived conversations', () => (
|
|||
|
||||
story.add('Archive: searching a conversation', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Archive,
|
||||
archivedConversations: defaultConversations,
|
||||
|
@ -438,7 +441,7 @@ story.add('Archive: searching a conversation', () => (
|
|||
|
||||
story.add('Compose: no contacts or groups', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
|
@ -452,7 +455,7 @@ story.add('Compose: no contacts or groups', () => (
|
|||
|
||||
story.add('Compose: some contacts, no groups, no search term', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: defaultConversations,
|
||||
|
@ -466,7 +469,7 @@ story.add('Compose: some contacts, no groups, no search term', () => (
|
|||
|
||||
story.add('Compose: some contacts, no groups, with a search term', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: defaultConversations,
|
||||
|
@ -480,7 +483,7 @@ story.add('Compose: some contacts, no groups, with a search term', () => (
|
|||
|
||||
story.add('Compose: some groups, no contacts, no search term', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
|
@ -494,7 +497,7 @@ story.add('Compose: some groups, no contacts, no search term', () => (
|
|||
|
||||
story.add('Compose: some groups, no contacts, with search term', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
|
@ -508,7 +511,7 @@ story.add('Compose: some groups, no contacts, with search term', () => (
|
|||
|
||||
story.add('Compose: some contacts, some groups, no search term', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: defaultConversations,
|
||||
|
@ -522,7 +525,7 @@ story.add('Compose: some contacts, some groups, no search term', () => (
|
|||
|
||||
story.add('Compose: some contacts, some groups, with a search term', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: defaultConversations,
|
||||
|
@ -538,7 +541,7 @@ story.add('Compose: some contacts, some groups, with a search term', () => (
|
|||
|
||||
story.add('Captcha dialog: required', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
pinnedConversations,
|
||||
|
@ -554,7 +557,7 @@ story.add('Captcha dialog: required', () => (
|
|||
|
||||
story.add('Captcha dialog: pending', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Inbox,
|
||||
pinnedConversations,
|
||||
|
@ -572,7 +575,7 @@ story.add('Captcha dialog: pending', () => (
|
|||
|
||||
story.add('Group Metadata: No Timer', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.SetGroupMetadata,
|
||||
groupAvatar: undefined,
|
||||
|
@ -590,7 +593,7 @@ story.add('Group Metadata: No Timer', () => (
|
|||
|
||||
story.add('Group Metadata: Regular Timer', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.SetGroupMetadata,
|
||||
groupAvatar: undefined,
|
||||
|
@ -608,7 +611,7 @@ story.add('Group Metadata: Regular Timer', () => (
|
|||
|
||||
story.add('Group Metadata: Custom Timer', () => (
|
||||
<LeftPane
|
||||
{...createProps({
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.SetGroupMetadata,
|
||||
groupAvatar: undefined,
|
||||
|
|
|
@ -23,8 +23,9 @@ import type { LeftPaneSetGroupMetadataPropsType } from './leftPane/LeftPaneSetGr
|
|||
import { LeftPaneSetGroupMetadataHelper } from './leftPane/LeftPaneSetGroupMetadataHelper';
|
||||
|
||||
import * as OS from '../OS';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||
import { ScrollBehavior } from '../types/Util';
|
||||
import type { BadgeType } from '../badges/types';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
@ -83,6 +84,7 @@ export type PropsType = {
|
|||
mode: LeftPaneMode.SetGroupMetadata;
|
||||
} & LeftPaneSetGroupMetadataPropsType);
|
||||
i18n: LocalizerType;
|
||||
badgesById: Record<string, BadgeType>;
|
||||
preferredWidthFromStorage: number;
|
||||
selectedConversationId: undefined | string;
|
||||
selectedMessageId: undefined | string;
|
||||
|
@ -90,6 +92,7 @@ export type PropsType = {
|
|||
canResizeLeftPane: boolean;
|
||||
challengeStatus: 'idle' | 'required' | 'pending';
|
||||
setChallengeStatus: (status: 'idle') => void;
|
||||
theme: ThemeType;
|
||||
|
||||
// Action Creators
|
||||
cantAddContactToGroup: (conversationId: string) => void;
|
||||
|
@ -143,6 +146,7 @@ export type PropsType = {
|
|||
};
|
||||
|
||||
export const LeftPane: React.FC<PropsType> = ({
|
||||
badgesById,
|
||||
cantAddContactToGroup,
|
||||
canResizeLeftPane,
|
||||
challengeStatus,
|
||||
|
@ -182,6 +186,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
startSearch,
|
||||
startNewConversationFromPhoneNumber,
|
||||
startSettingGroupMetadata,
|
||||
theme,
|
||||
toggleComposeEditingAvatar,
|
||||
toggleConversationInChooseMembers,
|
||||
updateSearchTerm,
|
||||
|
@ -565,6 +570,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
tabIndex={-1}
|
||||
>
|
||||
<ConversationList
|
||||
badgesById={badgesById}
|
||||
dimensions={{
|
||||
width,
|
||||
height: contentRect.bounds?.height || 0,
|
||||
|
@ -602,6 +608,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
startNewConversationFromPhoneNumber={
|
||||
startNewConversationFromPhoneNumber
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -280,6 +280,7 @@ story.add('Conversation Header', () => (
|
|||
getConversation={() => ({
|
||||
acceptedMessageRequest: true,
|
||||
avatarPath: '/fixtures/kitten-1-64-64.jpg',
|
||||
badges: [],
|
||||
id: '1234',
|
||||
isMe: false,
|
||||
name: 'Test',
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ContactModal } from './ContactModal';
|
|||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import { getFakeBadges } from '../../test-both/helpers/getFakeBadge';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -28,6 +29,7 @@ const defaultContact: ConversationType = getDefaultConversation({
|
|||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
||||
badges: overrideProps.badges || [],
|
||||
contact: overrideProps.contact || defaultContact,
|
||||
hideContactModal: action('hideContactModal'),
|
||||
i18n,
|
||||
|
@ -86,3 +88,11 @@ story.add('Viewing self', () => {
|
|||
|
||||
return <ContactModal {...props} />;
|
||||
});
|
||||
|
||||
story.add('With badges', () => {
|
||||
const props = createProps({
|
||||
badges: getFakeBadges(2),
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
});
|
||||
|
|
|
@ -3,17 +3,21 @@
|
|||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { About } from './About';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { AvatarLightbox } from '../AvatarLightbox';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import { Modal } from '../Modal';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { BadgeDialog } from '../BadgeDialog';
|
||||
import type { BadgeType } from '../../badges/types';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
|
||||
export type PropsDataType = {
|
||||
areWeAdmin: boolean;
|
||||
badges: ReadonlyArray<BadgeType>;
|
||||
contact?: ConversationType;
|
||||
conversationId?: string;
|
||||
readonly i18n: LocalizerType;
|
||||
|
@ -38,8 +42,15 @@ type PropsActionType = {
|
|||
|
||||
export type PropsType = PropsDataType & PropsActionType;
|
||||
|
||||
enum ContactModalView {
|
||||
Default,
|
||||
ShowingAvatar,
|
||||
ShowingBadges,
|
||||
}
|
||||
|
||||
export const ContactModal = ({
|
||||
areWeAdmin,
|
||||
badges,
|
||||
contact,
|
||||
conversationId,
|
||||
hideContactModal,
|
||||
|
@ -56,7 +67,7 @@ export const ContactModal = ({
|
|||
throw new Error('Contact modal opened without a matching contact');
|
||||
}
|
||||
|
||||
const [showingAvatar, setShowingAvatar] = useState(false);
|
||||
const [view, setView] = useState(ContactModalView.Default);
|
||||
const [confirmToggleAdmin, setConfirmToggleAdmin] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -66,135 +77,158 @@ export const ContactModal = ({
|
|||
}
|
||||
}, [conversationId, updateConversationModelSharedGroups]);
|
||||
|
||||
if (showingAvatar) {
|
||||
return (
|
||||
<AvatarLightbox
|
||||
avatarColor={contact.color}
|
||||
avatarPath={contact.avatarPath}
|
||||
conversationTitle={contact.title}
|
||||
i18n={i18n}
|
||||
onClose={() => setShowingAvatar(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
switch (view) {
|
||||
case ContactModalView.Default: {
|
||||
const preferredBadge: undefined | BadgeType = badges[0];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
moduleClassName="ContactModal__modal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={hideContactModal}
|
||||
>
|
||||
<div className="ContactModal">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
return (
|
||||
<Modal
|
||||
moduleClassName="ContactModal__modal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
isMe={contact.isMe}
|
||||
name={contact.name}
|
||||
profileName={contact.profileName}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={96}
|
||||
title={contact.title}
|
||||
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||
onClick={() => setShowingAvatar(true)}
|
||||
/>
|
||||
<div className="ContactModal__name">{contact.title}</div>
|
||||
<div className="module-about__container">
|
||||
<About text={contact.about} />
|
||||
</div>
|
||||
{contact.phoneNumber && (
|
||||
<div className="ContactModal__info">{contact.phoneNumber}</div>
|
||||
)}
|
||||
{!contact.isMe && (
|
||||
<div className="ContactModal__info">
|
||||
<SharedGroupNames
|
||||
onClose={hideContactModal}
|
||||
>
|
||||
<div className="ContactModal">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
badge={preferredBadge}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
sharedGroupNames={contact.sharedGroupNames || []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="ContactModal__button-container">
|
||||
<button
|
||||
type="button"
|
||||
className="ContactModal__button ContactModal__send-message"
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
openConversationInternal({ conversationId: contact.id });
|
||||
}}
|
||||
>
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__send-message__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('ContactModal--message')}</span>
|
||||
</button>
|
||||
{!contact.isMe && (
|
||||
<button
|
||||
type="button"
|
||||
className="ContactModal__button ContactModal__safety-number"
|
||||
isMe={contact.isMe}
|
||||
name={contact.name}
|
||||
profileName={contact.profileName}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={96}
|
||||
title={contact.title}
|
||||
unblurredAvatarPath={contact.unblurredAvatarPath}
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
toggleSafetyNumberModal(contact.id);
|
||||
setView(
|
||||
preferredBadge
|
||||
? ContactModalView.ShowingBadges
|
||||
: ContactModalView.ShowingAvatar
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__safety-number__bubble-icon" />
|
||||
/>
|
||||
<div className="ContactModal__name">{contact.title}</div>
|
||||
<div className="module-about__container">
|
||||
<About text={contact.about} />
|
||||
</div>
|
||||
{contact.phoneNumber && (
|
||||
<div className="ContactModal__info">{contact.phoneNumber}</div>
|
||||
)}
|
||||
{!contact.isMe && (
|
||||
<div className="ContactModal__info">
|
||||
<SharedGroupNames
|
||||
i18n={i18n}
|
||||
sharedGroupNames={contact.sharedGroupNames || []}
|
||||
/>
|
||||
</div>
|
||||
<span>{i18n('showSafetyNumber')}</span>
|
||||
</button>
|
||||
)}
|
||||
{!contact.isMe && areWeAdmin && isMember && conversationId && (
|
||||
<>
|
||||
)}
|
||||
<div className="ContactModal__button-container">
|
||||
<button
|
||||
type="button"
|
||||
className="ContactModal__button ContactModal__make-admin"
|
||||
onClick={() => setConfirmToggleAdmin(true)}
|
||||
className="ContactModal__button ContactModal__send-message"
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
openConversationInternal({ conversationId: contact.id });
|
||||
}}
|
||||
>
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__make-admin__bubble-icon" />
|
||||
<div className="ContactModal__send-message__bubble-icon" />
|
||||
</div>
|
||||
{isAdmin ? (
|
||||
<span>{i18n('ContactModal--rm-admin')}</span>
|
||||
) : (
|
||||
<span>{i18n('ContactModal--make-admin')}</span>
|
||||
)}
|
||||
<span>{i18n('ContactModal--message')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ContactModal__button ContactModal__remove-from-group"
|
||||
onClick={() =>
|
||||
removeMemberFromGroup(conversationId, contact.id)
|
||||
}
|
||||
{!contact.isMe && (
|
||||
<button
|
||||
type="button"
|
||||
className="ContactModal__button ContactModal__safety-number"
|
||||
onClick={() => {
|
||||
hideContactModal();
|
||||
toggleSafetyNumberModal(contact.id);
|
||||
}}
|
||||
>
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__safety-number__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('showSafetyNumber')}</span>
|
||||
</button>
|
||||
)}
|
||||
{!contact.isMe && areWeAdmin && isMember && conversationId && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="ContactModal__button ContactModal__make-admin"
|
||||
onClick={() => setConfirmToggleAdmin(true)}
|
||||
>
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__make-admin__bubble-icon" />
|
||||
</div>
|
||||
{isAdmin ? (
|
||||
<span>{i18n('ContactModal--rm-admin')}</span>
|
||||
) : (
|
||||
<span>{i18n('ContactModal--make-admin')}</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ContactModal__button ContactModal__remove-from-group"
|
||||
onClick={() =>
|
||||
removeMemberFromGroup(conversationId, contact.id)
|
||||
}
|
||||
>
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__remove-from-group__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('ContactModal--remove-from-group')}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{confirmToggleAdmin && conversationId && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () => toggleAdmin(conversationId, contact.id),
|
||||
text: isAdmin
|
||||
? i18n('ContactModal--rm-admin')
|
||||
: i18n('ContactModal--make-admin'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => setConfirmToggleAdmin(false)}
|
||||
>
|
||||
<div className="ContactModal__bubble-icon">
|
||||
<div className="ContactModal__remove-from-group__bubble-icon" />
|
||||
</div>
|
||||
<span>{i18n('ContactModal--remove-from-group')}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{confirmToggleAdmin && conversationId && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () => toggleAdmin(conversationId, contact.id),
|
||||
text: isAdmin
|
||||
? i18n('ContactModal--rm-admin')
|
||||
: i18n('ContactModal--make-admin'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => setConfirmToggleAdmin(false)}
|
||||
>
|
||||
{isAdmin
|
||||
? i18n('ContactModal--rm-admin-info', [contact.title])
|
||||
: i18n('ContactModal--make-admin-info', [contact.title])}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
{isAdmin
|
||||
? i18n('ContactModal--rm-admin-info', [contact.title])
|
||||
: i18n('ContactModal--make-admin-info', [contact.title])}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
case ContactModalView.ShowingAvatar:
|
||||
return (
|
||||
<AvatarLightbox
|
||||
avatarColor={contact.color}
|
||||
avatarPath={contact.avatarPath}
|
||||
conversationTitle={contact.title}
|
||||
i18n={i18n}
|
||||
onClose={() => setView(ContactModalView.Default)}
|
||||
/>
|
||||
);
|
||||
case ContactModalView.ShowingBadges:
|
||||
return (
|
||||
<BadgeDialog
|
||||
badges={badges}
|
||||
firstName={contact.firstName}
|
||||
i18n={i18n}
|
||||
onClose={() => setView(ContactModalView.Default)}
|
||||
title={contact.title}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
throw missingCaseError(view);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
@ -11,6 +11,7 @@ import { getDefaultConversation } from '../../test-both/helpers/getDefaultConver
|
|||
import { getRandomColor } from '../../test-both/helpers/getRandomColor';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
|
||||
import {
|
||||
ConversationHeader,
|
||||
OutgoingCallButtonStyle,
|
||||
|
@ -25,7 +26,7 @@ type ConversationHeaderStory = {
|
|||
description: string;
|
||||
items: Array<{
|
||||
title: string;
|
||||
props: ComponentProps<typeof ConversationHeader>;
|
||||
props: Omit<ComponentProps<typeof ConversationHeader>, 'theme'>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
@ -317,15 +318,18 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
stories.forEach(({ title, description, items }) =>
|
||||
book.add(
|
||||
title,
|
||||
() =>
|
||||
items.map(({ title: subtitle, props }, i) => {
|
||||
() => {
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return items.map(({ title: subtitle, props }, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
{subtitle ? <h3>{subtitle}</h3> : null}
|
||||
<ConversationHeader {...props} />
|
||||
<ConversationHeader {...props} theme={theme} />
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
});
|
||||
},
|
||||
{
|
||||
docs: description,
|
||||
}
|
||||
|
|
|
@ -17,8 +17,9 @@ import { DisappearingTimeDialog } from '../DisappearingTimeDialog';
|
|||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { InContactsIcon } from '../InContactsIcon';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import type { BadgeType } from '../../badges/types';
|
||||
import { getMuteOptions } from '../../util/getMuteOptions';
|
||||
import * as expirationTimer from '../../util/expirationTimer';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
@ -32,11 +33,13 @@ export enum OutgoingCallButtonStyle {
|
|||
}
|
||||
|
||||
export type PropsDataType = {
|
||||
badge?: BadgeType;
|
||||
conversationTitle?: string;
|
||||
isMissingMandatoryProfileSharing?: boolean;
|
||||
outgoingCallButtonStyle: OutgoingCallButtonStyle;
|
||||
showBackButton?: boolean;
|
||||
isSMSOnly?: boolean;
|
||||
theme: ThemeType;
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
|
@ -190,6 +193,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
badge,
|
||||
color,
|
||||
i18n,
|
||||
type,
|
||||
|
@ -198,6 +202,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
theme,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
} = this.props;
|
||||
|
@ -207,6 +212,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
badge={badge}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
i18n={i18n}
|
||||
|
@ -218,6 +224,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_TWO}
|
||||
theme={theme}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { action } from '@storybook/addon-actions';
|
|||
import { ConversationHero } from './ConversationHero';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -22,11 +23,18 @@ const getPhoneNumber = () => text('phoneNumber', '+1 (646) 327-2700');
|
|||
|
||||
const updateSharedGroups = action('updateSharedGroups');
|
||||
|
||||
const Wrapper = (
|
||||
props: Omit<React.ComponentProps<typeof ConversationHero>, 'theme'>
|
||||
) => {
|
||||
const theme = React.useContext(StorybookThemeContext);
|
||||
return <ConversationHero {...props} theme={theme} />;
|
||||
};
|
||||
|
||||
storiesOf('Components/Conversation/ConversationHero', module)
|
||||
.add('Direct (Five Other Groups)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
|
@ -54,7 +62,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Direct (Four Other Groups)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
|
@ -81,7 +89,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Direct (Three Other Groups)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
|
@ -103,7 +111,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Direct (Two Other Groups)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
|
@ -125,7 +133,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Direct (One Other Group)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
|
@ -147,7 +155,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Direct (No Groups, Name)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
|
@ -169,7 +177,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Direct (No Groups, Just Profile)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
|
@ -191,7 +199,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Direct (No Groups, Just Phone Number)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
|
@ -213,7 +221,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Direct (No Groups, No Data)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={text('title', 'Unknown contact')}
|
||||
|
@ -234,7 +242,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Direct (No Groups, No Data, Not Accepted)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={text('title', 'Unknown contact')}
|
||||
|
@ -255,7 +263,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Group (many members)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
|
@ -274,7 +282,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Group (one member)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
|
@ -293,7 +301,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Group (zero members)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
|
@ -313,7 +321,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Group (long group description)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
|
@ -333,7 +341,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Group (No name)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
|
@ -352,7 +360,7 @@ storiesOf('Components/Conversation/ConversationHero', module)
|
|||
.add('Note to Self', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
<Wrapper
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
isMe
|
||||
|
|
|
@ -8,7 +8,7 @@ import { ContactName } from './ContactName';
|
|||
import { About } from './About';
|
||||
import { GroupDescription } from './GroupDescription';
|
||||
import { SharedGroupNames } from '../SharedGroupNames';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||
|
@ -28,6 +28,7 @@ export type Props = {
|
|||
unblurAvatar: () => void;
|
||||
unblurredAvatarPath?: string;
|
||||
updateSharedGroups: () => unknown;
|
||||
theme: ThemeType;
|
||||
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
|
||||
|
||||
const renderMembershipRow = ({
|
||||
|
@ -98,6 +99,7 @@ export const ConversationHero = ({
|
|||
about,
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
badge,
|
||||
color,
|
||||
conversationType,
|
||||
groupDescription,
|
||||
|
@ -107,6 +109,7 @@ export const ConversationHero = ({
|
|||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
theme,
|
||||
title,
|
||||
onHeightChange,
|
||||
unblurAvatar,
|
||||
|
@ -180,6 +183,7 @@ export const ConversationHero = ({
|
|||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
badge={badge}
|
||||
blur={avatarBlur}
|
||||
className="module-conversation-hero__avatar"
|
||||
color={color}
|
||||
|
@ -192,6 +196,7 @@ export const ConversationHero = ({
|
|||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={112}
|
||||
theme={theme}
|
||||
title={title}
|
||||
/>
|
||||
<h1 className="module-conversation-hero__profile-name">
|
||||
|
|
|
@ -15,6 +15,7 @@ import type { PropsType } from './Timeline';
|
|||
import { Timeline } from './Timeline';
|
||||
import type { TimelineItemType } from './TimelineItem';
|
||||
import { TimelineItem } from './TimelineItem';
|
||||
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
|
||||
import { ConversationHero } from './ConversationHero';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { getRandomColor } from '../../test-both/helpers/getRandomColor';
|
||||
|
@ -412,24 +413,31 @@ const getAvatarPath = () =>
|
|||
text('avatarPath', '/fixtures/kitten-4-112-112.jpg');
|
||||
const getPhoneNumber = () => text('phoneNumber', '+1 (808) 555-1234');
|
||||
|
||||
const renderHeroRow = () => (
|
||||
<ConversationHero
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={getTitle()}
|
||||
avatarPath={getAvatarPath()}
|
||||
name={getName()}
|
||||
profileName={getProfileName()}
|
||||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
onHeightChange={action('onHeightChange in ConversationHero')}
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={noop}
|
||||
/>
|
||||
);
|
||||
const renderHeroRow = () => {
|
||||
const Wrapper = () => {
|
||||
const theme = React.useContext(StorybookThemeContext);
|
||||
return (
|
||||
<ConversationHero
|
||||
about={getAbout()}
|
||||
acceptedMessageRequest
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title={getTitle()}
|
||||
avatarPath={getAvatarPath()}
|
||||
name={getName()}
|
||||
profileName={getProfileName()}
|
||||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
onHeightChange={action('onHeightChange in ConversationHero')}
|
||||
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
|
||||
theme={theme}
|
||||
unblurAvatar={action('unblurAvatar')}
|
||||
updateSharedGroups={noop}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return <Wrapper />;
|
||||
};
|
||||
const renderLoadingRow = () => <TimelineLoadingRow state="loading" />;
|
||||
const renderTypingBubble = () => (
|
||||
<TypingBubble
|
||||
|
|
|
@ -14,6 +14,7 @@ import enMessages from '../../../../_locales/en/messages.json';
|
|||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
import { AddGroupMembersModal } from './AddGroupMembersModal';
|
||||
import { RequestState } from './util';
|
||||
import { ThemeType } from '../../../types/Util';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -37,6 +38,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
action('onMakeRequest')(conversationIds);
|
||||
},
|
||||
requestState: RequestState.Inactive,
|
||||
theme: ThemeType.light,
|
||||
...overrideProps,
|
||||
});
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { FunctionComponent } from 'react';
|
|||
import React, { useMemo, useReducer } from 'react';
|
||||
import { without } from 'lodash';
|
||||
|
||||
import type { LocalizerType } from '../../../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util';
|
||||
import {
|
||||
AddGroupMemberErrorDialog,
|
||||
AddGroupMemberErrorDialogMode,
|
||||
|
@ -35,6 +35,7 @@ type PropsType = {
|
|||
makeRequest: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
||||
onClose: () => void;
|
||||
requestState: RequestState;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
enum Stage {
|
||||
|
@ -151,6 +152,7 @@ export const AddGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
onClose,
|
||||
makeRequest,
|
||||
requestState,
|
||||
theme,
|
||||
}) => {
|
||||
const maxGroupSize = getMaximumNumberOfContacts();
|
||||
const maxRecommendedGroupSize = getRecommendedMaximumNumberOfContacts();
|
||||
|
@ -284,6 +286,7 @@ export const AddGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
selectedContacts={selectedContacts}
|
||||
setCantAddContactForModal={setCantAddContactForModal}
|
||||
setSearchTerm={setSearchTerm}
|
||||
theme={theme}
|
||||
toggleSelectedContact={toggleSelectedContact}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@ import React, { useEffect, useMemo, useState, useRef } from 'react';
|
|||
import type { MeasuredComponentProps } from 'react-measure';
|
||||
import Measure from 'react-measure';
|
||||
|
||||
import type { LocalizerType } from '../../../../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../../../../types/Util';
|
||||
import { assert } from '../../../../util/assert';
|
||||
import { getOwn } from '../../../../util/getOwn';
|
||||
import { refMerger } from '../../../../util/refMerger';
|
||||
|
@ -38,6 +38,7 @@ type PropsType = {
|
|||
_: Readonly<undefined | ConversationType>
|
||||
) => void;
|
||||
setSearchTerm: (_: string) => void;
|
||||
theme: ThemeType;
|
||||
toggleSelectedContact: (conversationId: string) => void;
|
||||
};
|
||||
|
||||
|
@ -55,6 +56,7 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
selectedContacts,
|
||||
setCantAddContactForModal,
|
||||
setSearchTerm,
|
||||
theme,
|
||||
toggleSelectedContact,
|
||||
}) => {
|
||||
const [focusRef] = useRestoreFocus();
|
||||
|
@ -227,6 +229,7 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
startNewConversationFromPhoneNumber={shouldNeverBeCalled}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { Props } from './ConversationDetails';
|
|||
import { ConversationDetails } from './ConversationDetails';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
import { ThemeType } from '../../../types/Util';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -55,6 +56,7 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
|||
isMe: i === 2,
|
||||
}),
|
||||
})),
|
||||
preferredBadgeByConversation: {},
|
||||
pendingApprovalMemberships: times(8, () => ({
|
||||
member: getDefaultConversation(),
|
||||
})),
|
||||
|
@ -92,6 +94,7 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
|||
'onOutgoingVideoCallInConversation'
|
||||
),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
theme: ThemeType.light,
|
||||
});
|
||||
|
||||
story.add('Basic', () => {
|
||||
|
|
|
@ -9,8 +9,9 @@ import type { ConversationType } from '../../../state/ducks/conversations';
|
|||
import { assert } from '../../../util/assert';
|
||||
import { getMutedUntilText } from '../../../util/getMutedUntilText';
|
||||
|
||||
import type { LocalizerType } from '../../../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util';
|
||||
import type { MediaItemType } from '../../../types/MediaItem';
|
||||
import type { BadgeType } from '../../../badges/types';
|
||||
import { CapabilityError } from '../../../types/errors';
|
||||
import { missingCaseError } from '../../../util/missingCaseError';
|
||||
|
||||
|
@ -53,6 +54,7 @@ enum ModalState {
|
|||
|
||||
export type StateProps = {
|
||||
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
||||
badges?: ReadonlyArray<BadgeType>;
|
||||
canEditGroupInfo: boolean;
|
||||
candidateContactsToAdd: Array<ConversationType>;
|
||||
conversation?: ConversationType;
|
||||
|
@ -62,6 +64,7 @@ export type StateProps = {
|
|||
isGroup: boolean;
|
||||
loadRecentMediaItems: (limit: number) => void;
|
||||
memberships: Array<GroupV2Membership>;
|
||||
preferredBadgeByConversation: Record<string, BadgeType>;
|
||||
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
|
||||
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
|
||||
setDisappearingMessages: (seconds: number) => void;
|
||||
|
@ -85,6 +88,7 @@ export type StateProps = {
|
|||
onBlock: () => void;
|
||||
onLeave: () => void;
|
||||
onUnblock: () => void;
|
||||
theme: ThemeType;
|
||||
userAvatarData: Array<AvatarDataType>;
|
||||
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
|
||||
onOutgoingAudioCallInConversation: () => unknown;
|
||||
|
@ -104,6 +108,7 @@ export type Props = StateProps & ActionProps;
|
|||
|
||||
export const ConversationDetails: React.ComponentType<Props> = ({
|
||||
addMembers,
|
||||
badges,
|
||||
canEditGroupInfo,
|
||||
candidateContactsToAdd,
|
||||
conversation,
|
||||
|
@ -121,6 +126,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
onUnblock,
|
||||
pendingApprovalMemberships,
|
||||
pendingMemberships,
|
||||
preferredBadgeByConversation,
|
||||
replaceAvatar,
|
||||
saveAvatarToDisk,
|
||||
searchInConversation,
|
||||
|
@ -134,6 +140,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
showGroupV2Permissions,
|
||||
showLightboxForMedia,
|
||||
showPendingInvites,
|
||||
theme,
|
||||
toggleSafetyNumberModal,
|
||||
updateGroupAttributes,
|
||||
userAvatarData,
|
||||
|
@ -256,6 +263,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
setEditGroupAttributesRequestState(RequestState.Inactive);
|
||||
}}
|
||||
requestState={addGroupMembersRequestState}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -311,6 +319,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
)}
|
||||
|
||||
<ConversationDetailsHeader
|
||||
badges={badges}
|
||||
canEdit={canEditGroupInfo}
|
||||
conversation={conversation}
|
||||
i18n={i18n}
|
||||
|
@ -324,6 +333,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
: ModalState.EditingGroupDescription
|
||||
);
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<div className="ConversationDetails__header-buttons">
|
||||
|
@ -456,10 +466,12 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
conversationId={conversation.id}
|
||||
i18n={i18n}
|
||||
memberships={memberships}
|
||||
preferredBadgeByConversation={preferredBadgeByConversation}
|
||||
showContactModal={showContactModal}
|
||||
startAddingNewMembers={() => {
|
||||
setModalState(ModalState.AddingGroupMembers);
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -8,8 +8,10 @@ import { action } from '@storybook/addon-actions';
|
|||
import { number, text } from '@storybook/addon-knobs';
|
||||
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
import { getFakeBadges } from '../../../test-both/helpers/getFakeBadge';
|
||||
import { setupI18n } from '../../../util/setupI18n';
|
||||
import enMessages from '../../../../_locales/en/messages.json';
|
||||
import { StorybookThemeContext } from '../../../../.storybook/StorybookThemeContext';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
|
||||
import type { Props } from './ConversationDetailsHeader';
|
||||
|
@ -34,61 +36,46 @@ const createConversation = (): ConversationType =>
|
|||
),
|
||||
});
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
conversation: createConversation(),
|
||||
i18n,
|
||||
canEdit: false,
|
||||
startEditing: action('startEditing'),
|
||||
memberships: new Array(number('conversation members length', 0)),
|
||||
isGroup: true,
|
||||
isMe: false,
|
||||
...overrideProps,
|
||||
});
|
||||
|
||||
story.add('Basic', () => {
|
||||
const props = createProps();
|
||||
|
||||
return <ConversationDetailsHeader {...props} />;
|
||||
});
|
||||
|
||||
story.add('Editable', () => {
|
||||
const props = createProps({ canEdit: true });
|
||||
|
||||
return <ConversationDetailsHeader {...props} />;
|
||||
});
|
||||
|
||||
story.add('Basic no-description', () => {
|
||||
const props = createProps();
|
||||
const Wrapper = (overrideProps: Partial<Props>) => {
|
||||
const theme = React.useContext(StorybookThemeContext);
|
||||
|
||||
return (
|
||||
<ConversationDetailsHeader
|
||||
{...props}
|
||||
conversation={getDefaultConversation({
|
||||
title: 'My Group',
|
||||
type: 'group',
|
||||
})}
|
||||
conversation={createConversation()}
|
||||
i18n={i18n}
|
||||
canEdit={false}
|
||||
startEditing={action('startEditing')}
|
||||
memberships={new Array(number('conversation members length', 0))}
|
||||
isGroup
|
||||
isMe={false}
|
||||
theme={theme}
|
||||
{...overrideProps}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
story.add('Editable no-description', () => {
|
||||
const props = createProps({ canEdit: true });
|
||||
story.add('Basic', () => <Wrapper />);
|
||||
|
||||
return (
|
||||
<ConversationDetailsHeader
|
||||
{...props}
|
||||
conversation={getDefaultConversation({
|
||||
title: 'My Group',
|
||||
type: 'group',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
story.add('Editable', () => <Wrapper canEdit />);
|
||||
|
||||
story.add('1:1', () => (
|
||||
<ConversationDetailsHeader {...createProps()} isGroup={false} />
|
||||
story.add('Basic no-description', () => (
|
||||
<Wrapper
|
||||
conversation={getDefaultConversation({
|
||||
title: 'My Group',
|
||||
type: 'group',
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Note to self', () => (
|
||||
<ConversationDetailsHeader {...createProps()} isMe />
|
||||
story.add('Editable no-description', () => (
|
||||
<Wrapper
|
||||
conversation={getDefaultConversation({
|
||||
title: 'My Group',
|
||||
type: 'group',
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('1:1', () => <Wrapper isGroup={false} badges={getFakeBadges(3)} />);
|
||||
|
||||
story.add('Note to self', () => <Wrapper isMe />);
|
||||
|
|
|
@ -11,10 +11,13 @@ import { Emojify } from '../Emojify';
|
|||
import { GroupDescription } from '../GroupDescription';
|
||||
import { About } from '../About';
|
||||
import type { GroupV2Membership } from './ConversationDetailsMembershipList';
|
||||
import type { LocalizerType } from '../../../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util';
|
||||
import { bemGenerator } from './util';
|
||||
import { BadgeDialog } from '../../BadgeDialog';
|
||||
import type { BadgeType } from '../../../badges/types';
|
||||
|
||||
export type Props = {
|
||||
badges?: ReadonlyArray<BadgeType>;
|
||||
canEdit: boolean;
|
||||
conversation: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
|
@ -22,11 +25,18 @@ export type Props = {
|
|||
isMe: boolean;
|
||||
memberships: Array<GroupV2Membership>;
|
||||
startEditing: (isGroupTitle: boolean) => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
enum ConversationDetailsHeaderActiveModal {
|
||||
ShowingAvatar,
|
||||
ShowingBadges,
|
||||
}
|
||||
|
||||
const bem = bemGenerator('ConversationDetails-header');
|
||||
|
||||
export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
||||
badges,
|
||||
canEdit,
|
||||
conversation,
|
||||
i18n,
|
||||
|
@ -34,9 +44,13 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
|||
isMe,
|
||||
memberships,
|
||||
startEditing,
|
||||
theme,
|
||||
}) => {
|
||||
const [showingAvatar, setShowingAvatar] = useState(false);
|
||||
const [activeModal, setActiveModal] = useState<
|
||||
undefined | ConversationDetailsHeaderActiveModal
|
||||
>();
|
||||
|
||||
let preferredBadge: undefined | BadgeType;
|
||||
let subtitle: ReactNode;
|
||||
if (isGroup) {
|
||||
if (conversation.groupDescription) {
|
||||
|
@ -65,17 +79,26 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
preferredBadge = badges?.[0];
|
||||
}
|
||||
|
||||
const avatar = (
|
||||
<Avatar
|
||||
badge={preferredBadge}
|
||||
conversationType={conversation.type}
|
||||
i18n={i18n}
|
||||
size={80}
|
||||
{...conversation}
|
||||
noteToSelf={isMe}
|
||||
onClick={() => setShowingAvatar(true)}
|
||||
onClick={() => {
|
||||
setActiveModal(
|
||||
preferredBadge
|
||||
? ConversationDetailsHeaderActiveModal.ShowingBadges
|
||||
: ConversationDetailsHeaderActiveModal.ShowingAvatar
|
||||
);
|
||||
}}
|
||||
sharedGroupNames={[]}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -87,22 +110,44 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
const avatarLightbox =
|
||||
showingAvatar && !isMe ? (
|
||||
<AvatarLightbox
|
||||
avatarColor={conversation.color}
|
||||
avatarPath={conversation.avatarPath}
|
||||
conversationTitle={conversation.title}
|
||||
i18n={i18n}
|
||||
isGroup={isGroup}
|
||||
onClose={() => setShowingAvatar(false)}
|
||||
/>
|
||||
) : null;
|
||||
let modal: ReactNode;
|
||||
switch (activeModal) {
|
||||
case ConversationDetailsHeaderActiveModal.ShowingAvatar:
|
||||
modal = (
|
||||
<AvatarLightbox
|
||||
avatarColor={conversation.color}
|
||||
avatarPath={conversation.avatarPath}
|
||||
conversationTitle={conversation.title}
|
||||
i18n={i18n}
|
||||
isGroup={isGroup}
|
||||
onClose={() => {
|
||||
setActiveModal(undefined);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case ConversationDetailsHeaderActiveModal.ShowingBadges:
|
||||
modal = (
|
||||
<BadgeDialog
|
||||
badges={badges || []}
|
||||
firstName={conversation.firstName}
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setActiveModal(undefined);
|
||||
}}
|
||||
title={conversation.title}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
modal = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (canEdit) {
|
||||
return (
|
||||
<div className={bem('root')}>
|
||||
{avatarLightbox}
|
||||
{modal}
|
||||
{avatar}
|
||||
<button
|
||||
type="button"
|
||||
|
@ -136,7 +181,7 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
|||
|
||||
return (
|
||||
<div className={bem('root')}>
|
||||
{avatarLightbox}
|
||||
{modal}
|
||||
{avatar}
|
||||
{contents}
|
||||
<div className={bem('subtitle')}>{subtitle}</div>
|
||||
|
|
|
@ -11,6 +11,9 @@ import { number } from '@storybook/addon-knobs';
|
|||
import { setupI18n } from '../../../util/setupI18n';
|
||||
import enMessages from '../../../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
import { getFakeBadge } from '../../../test-both/helpers/getFakeBadge';
|
||||
import { ThemeType } from '../../../types/Util';
|
||||
import type { BadgeType } from '../../../badges/types';
|
||||
|
||||
import type {
|
||||
Props,
|
||||
|
@ -47,8 +50,21 @@ const createProps = (overrideProps: Partial<Props>): Props => ({
|
|||
conversationId: '123',
|
||||
i18n,
|
||||
memberships: overrideProps.memberships || [],
|
||||
preferredBadgeByConversation:
|
||||
overrideProps.preferredBadgeByConversation ||
|
||||
(overrideProps.memberships || []).reduce(
|
||||
(result: Record<string, BadgeType>, { member }, index) =>
|
||||
(index + 1) % 3 === 0
|
||||
? {
|
||||
...result,
|
||||
[member.id]: getFakeBadge({ alternate: index % 2 !== 0 }),
|
||||
}
|
||||
: result,
|
||||
{}
|
||||
),
|
||||
showContactModal: action('showContactModal'),
|
||||
startAddingNewMembers: action('startAddingNewMembers'),
|
||||
theme: ThemeType.light,
|
||||
});
|
||||
|
||||
story.add('Few', () => {
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../../../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util';
|
||||
import { getOwn } from '../../../util/getOwn';
|
||||
|
||||
import type { BadgeType } from '../../../badges/types';
|
||||
import { Avatar } from '../../Avatar';
|
||||
import { Emojify } from '../Emojify';
|
||||
|
||||
|
@ -23,8 +26,10 @@ export type Props = {
|
|||
i18n: LocalizerType;
|
||||
maxShownMemberCount?: number;
|
||||
memberships: Array<GroupV2Membership>;
|
||||
preferredBadgeByConversation: Record<string, BadgeType>;
|
||||
showContactModal: (contactId: string, conversationId: string) => void;
|
||||
startAddingNewMembers?: () => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
|
@ -72,8 +77,10 @@ export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
|||
i18n,
|
||||
maxShownMemberCount = 5,
|
||||
memberships,
|
||||
preferredBadgeByConversation,
|
||||
showContactModal,
|
||||
startAddingNewMembers,
|
||||
theme,
|
||||
}) => {
|
||||
const [showAllMembers, setShowAllMembers] = React.useState<boolean>(false);
|
||||
const sortedMemberships = sortMemberships(memberships);
|
||||
|
@ -107,8 +114,10 @@ export const ConversationDetailsMembershipList: React.ComponentType<Props> = ({
|
|||
icon={
|
||||
<Avatar
|
||||
conversationType="direct"
|
||||
badge={getOwn(preferredBadgeByConversation, member.id)}
|
||||
i18n={i18n}
|
||||
size={32}
|
||||
theme={theme}
|
||||
{...member}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ const sortedGroupMembers = Array.from(Array(32)).map((_, i) =>
|
|||
const conversation: ConversationType = {
|
||||
acceptedMessageRequest: true,
|
||||
areWeAdmin: true,
|
||||
badges: [],
|
||||
id: '',
|
||||
lastUpdated: 0,
|
||||
markedUnread: false,
|
||||
|
|
|
@ -8,10 +8,11 @@ import { isBoolean, isNumber } from 'lodash';
|
|||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import type { BadgeType } from '../../badges/types';
|
||||
import { Timestamp } from '../conversation/Timestamp';
|
||||
import { isConversationUnread } from '../../util/isConversationUnread';
|
||||
import { cleanId } from '../_util';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
|
||||
const BASE_CLASS_NAME =
|
||||
|
@ -27,6 +28,7 @@ export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`;
|
|||
const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`;
|
||||
|
||||
type PropsType = {
|
||||
badge?: BadgeType;
|
||||
checked?: boolean;
|
||||
conversationType: 'group' | 'direct';
|
||||
disabled?: boolean;
|
||||
|
@ -42,6 +44,7 @@ type PropsType = {
|
|||
messageText?: ReactNode;
|
||||
messageTextIsAlwaysFullSize?: boolean;
|
||||
onClick?: () => void;
|
||||
theme?: ThemeType;
|
||||
unreadCount?: number;
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
|
@ -62,6 +65,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
function BaseConversationListItem({
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
badge,
|
||||
checked,
|
||||
color,
|
||||
conversationType,
|
||||
|
@ -82,6 +86,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
theme,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
unreadCount,
|
||||
|
@ -129,6 +134,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
badge={badge}
|
||||
color={color}
|
||||
conversationType={conversationType}
|
||||
noteToSelf={isAvatarNoteToSelf}
|
||||
|
@ -137,6 +143,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
theme={theme}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
|
|
|
@ -15,8 +15,9 @@ import { MessageBody } from '../conversation/MessageBody';
|
|||
import { ContactName } from '../conversation/ContactName';
|
||||
import { TypingAnimation } from '../conversation/TypingAnimation';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import type { BadgeType } from '../../badges/types';
|
||||
|
||||
const MESSAGE_STATUS_ICON_CLASS_NAME = `${MESSAGE_TEXT_CLASS_NAME}__status-icon`;
|
||||
|
||||
|
@ -36,6 +37,7 @@ export type PropsData = Pick<
|
|||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'badges'
|
||||
| 'color'
|
||||
| 'draftPreview'
|
||||
| 'id'
|
||||
|
@ -56,11 +58,14 @@ export type PropsData = Pick<
|
|||
| 'typingContact'
|
||||
| 'unblurredAvatarPath'
|
||||
| 'unreadCount'
|
||||
>;
|
||||
> & {
|
||||
badge?: BadgeType;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
onClick: (id: string) => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
||||
export type Props = PropsData & PropsHousekeeping;
|
||||
|
@ -69,6 +74,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
function ConversationListItem({
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
badge,
|
||||
color,
|
||||
draftPreview,
|
||||
i18n,
|
||||
|
@ -85,6 +91,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
profileName,
|
||||
sharedGroupNames,
|
||||
shouldShowDraft,
|
||||
theme,
|
||||
title,
|
||||
type,
|
||||
typingContact,
|
||||
|
@ -163,6 +170,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
<BaseConversationListItem
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
badge={badge}
|
||||
color={color}
|
||||
conversationType={type}
|
||||
headerDate={lastUpdated}
|
||||
|
@ -180,6 +188,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
theme={theme}
|
||||
title={title}
|
||||
unreadCount={unreadCount}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue