signal-desktop/ts/components/conversation/Timeline.stories.tsx

623 lines
17 KiB
TypeScript
Raw Normal View History

2022-01-26 23:05:26 +00:00
// Copyright 2020-2022 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { times } from 'lodash';
2021-06-01 23:30:25 +00:00
import { v4 as uuid } from 'uuid';
import { text, boolean, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
2021-09-18 00:30:08 +00:00
import { setupI18n } from '../../util/setupI18n';
2022-11-16 20:18:02 +00:00
import { DurationInSeconds } from '../../util/durations';
import enMessages from '../../../_locales/en/messages.json';
import type { PropsType } from './Timeline';
import { Timeline } from './Timeline';
import type { TimelineItemType } from './TimelineItem';
import { TimelineItem } from './TimelineItem';
import { ContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
2021-11-02 23:01:13 +00:00
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
import { ConversationHero } from './ConversationHero';
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from '../../state/smart/ContactSpoofingReviewDialog';
2021-04-21 16:31:12 +00:00
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
2021-08-06 00:17:05 +00:00
import { getRandomColor } from '../../test-both/helpers/getRandomColor';
import { TypingBubble } from './TypingBubble';
2021-06-01 23:30:25 +00:00
import { ContactSpoofingType } from '../../util/contactSpoofing';
import { ReadStatus } from '../../messages/MessageReadStatus';
import type { WidthBreakpoint } from '../_util';
import { ThemeType } from '../../types/Util';
import { TextDirection } from './Message';
2022-11-30 21:47:54 +00:00
import { PaymentEventKind } from '../../types/Payment';
import type { PropsData as TimelineMessageProps } from './TimelineMessage';
const i18n = setupI18n('en', enMessages);
2022-06-07 00:48:02 +00:00
export default {
title: 'Components/Conversation/Timeline',
};
2020-09-14 19:51:27 +00:00
// eslint-disable-next-line
const noop = () => {};
2022-11-30 21:47:54 +00:00
function mockMessageTimelineItem(
id: string,
data: Partial<TimelineMessageProps>
): TimelineItemType {
return {
type: 'message',
data: {
2022-11-30 21:47:54 +00:00
id,
2021-08-06 00:17:05 +00:00
author: getDefaultConversation({}),
2021-06-02 17:27:12 +00:00
canDeleteForEveryone: false,
canDownload: true,
canReact: true,
2021-06-02 17:27:12 +00:00
canReply: true,
canRetry: true,
2021-06-02 17:27:12 +00:00
conversationId: 'conversation-id',
2022-05-11 20:59:58 +00:00
conversationTitle: 'Conversation Title',
conversationType: 'group',
2022-11-30 21:47:54 +00:00
conversationColor: 'crimson',
direction: 'incoming',
2022-11-30 21:47:54 +00:00
status: 'sent',
text: 'Hello there from the new world!',
2021-06-02 17:27:12 +00:00
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],
readStatus: ReadStatus.Read,
2022-11-30 21:47:54 +00:00
canRetryDeleteForEveryone: true,
textDirection: TextDirection.Default,
2021-06-02 17:27:12 +00:00
timestamp: Date.now(),
2022-11-30 21:47:54 +00:00
...data,
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
2022-11-30 21:47:54 +00:00
};
}
const items: Record<string, TimelineItemType> = {
'id-1': mockMessageTimelineItem('id-1', {
author: getDefaultConversation({
phoneNumber: '(202) 555-2001',
}),
conversationColor: 'forest',
text: '🔥',
}),
'id-2': mockMessageTimelineItem('id-2', {
conversationColor: 'forest',
direction: 'incoming',
text: 'Hello there from the new world! http://somewhere.com',
}),
'id-2.5': {
type: 'unsupportedMessage',
data: {
canProcessNow: false,
contact: {
2021-06-02 17:27:12 +00:00
id: '061d3783-5736-4145-b1a2-6b6cf1156393',
isMe: false,
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Pig',
title: 'Mr. Pig',
},
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
2022-11-30 21:47:54 +00:00
'id-3': mockMessageTimelineItem('id-3', {}),
'id-4': {
type: 'timerNotification',
data: {
2021-06-02 17:27:12 +00:00
disabled: false,
2022-11-16 20:18:02 +00:00
expireTimer: DurationInSeconds.fromHours(2),
2021-06-02 17:27:12 +00:00
title: "It's Me",
type: 'fromMe',
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
'id-5': {
type: 'timerNotification',
data: {
2021-06-02 17:27:12 +00:00
disabled: false,
2022-11-16 20:18:02 +00:00
expireTimer: DurationInSeconds.fromHours(2),
2021-06-02 17:27:12 +00:00
title: '(202) 555-0000',
type: 'fromOther',
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
'id-6': {
type: 'safetyNumberNotification',
data: {
contact: {
id: '+1202555000',
2021-06-02 17:27:12 +00:00
title: 'Mr. Fire',
},
2021-06-02 17:27:12 +00:00
isGroup: true,
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
'id-7': {
type: 'verificationNotification',
data: {
contact: { title: 'Mrs. Ice' },
isLocal: true,
type: 'markVerified',
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
'id-8': {
type: 'groupNotification',
data: {
changes: [
{
type: 'name',
newName: 'Squirrels and their uses',
},
{
type: 'add',
contacts: [
getDefaultConversation({
phoneNumber: '(202) 555-0002',
title: 'Mr. Fire',
}),
getDefaultConversation({
phoneNumber: '(202) 555-0003',
title: 'Ms. Water',
}),
],
},
],
from: getDefaultConversation({
phoneNumber: '(202) 555-0001',
title: 'Mrs. Ice',
2021-06-02 17:27:12 +00:00
isMe: false,
}),
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
'id-9': {
type: 'resetSessionNotification',
data: null,
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
2022-11-30 21:47:54 +00:00
'id-10': mockMessageTimelineItem('id-10', {
conversationColor: 'plum',
direction: 'outgoing',
text: '🔥',
}),
'id-11': mockMessageTimelineItem('id-11', {
direction: 'outgoing',
status: 'read',
text: 'Hello there from the new world! http://somewhere.com',
}),
'id-12': mockMessageTimelineItem('id-12', {
direction: 'outgoing',
text: 'Hello there from the new world! 🔥',
}),
'id-13': mockMessageTimelineItem('id-13', {
direction: 'outgoing',
text: 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
}),
'id-14': mockMessageTimelineItem('id-14', {
direction: 'outgoing',
status: 'read',
text: 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
}),
'id-15': {
type: 'paymentEvent',
data: {
2022-11-30 21:47:54 +00:00
event: {
kind: PaymentEventKind.ActivationRequest,
},
sender: getDefaultConversation(),
conversation: getDefaultConversation(),
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
2022-11-30 21:47:54 +00:00
'id-16': {
type: 'paymentEvent',
data: {
2022-11-30 21:47:54 +00:00
event: {
kind: PaymentEventKind.Activation,
},
sender: getDefaultConversation(),
conversation: getDefaultConversation(),
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
2022-11-30 21:47:54 +00:00
'id-17': {
type: 'paymentEvent',
data: {
2022-11-30 21:47:54 +00:00
event: {
kind: PaymentEventKind.ActivationRequest,
},
sender: getDefaultConversation({
isMe: true,
}),
conversation: getDefaultConversation(),
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
2022-11-30 21:47:54 +00:00
'id-18': {
type: 'paymentEvent',
data: {
2022-11-30 21:47:54 +00:00
event: {
kind: PaymentEventKind.Activation,
},
sender: getDefaultConversation({
isMe: true,
}),
conversation: getDefaultConversation(),
},
2022-01-26 23:05:26 +00:00
timestamp: Date.now(),
},
2022-11-30 21:47:54 +00:00
'id-19': mockMessageTimelineItem('id-19', {
direction: 'outgoing',
status: 'read',
payment: {
kind: PaymentEventKind.Notification,
note: 'Thanks',
},
2022-11-30 21:47:54 +00:00
}),
2021-06-02 17:27:12 +00:00
};
const actions = () => ({
2021-06-01 23:30:25 +00:00
acknowledgeGroupMemberNameCollisions: action(
'acknowledgeGroupMemberNameCollisions'
),
blockGroupLinkRequests: action('blockGroupLinkRequests'),
checkForAccount: action('checkForAccount'),
2021-10-26 22:59:08 +00:00
clearInvitedUuidsForNewlyCreatedGroup: action(
'clearInvitedUuidsForNewlyCreatedGroup'
2021-03-03 20:09:58 +00:00
),
setIsNearBottom: action('setIsNearBottom'),
learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'),
loadOlderMessages: action('loadOlderMessages'),
loadNewerMessages: action('loadNewerMessages'),
loadNewestMessages: action('loadNewestMessages'),
markMessageRead: action('markMessageRead'),
selectMessage: action('selectMessage'),
clearSelectedMessage: action('clearSelectedMessage'),
updateSharedGroups: action('updateSharedGroups'),
reactToMessage: action('reactToMessage'),
setQuoteByMessageId: action('setQuoteByMessageId'),
retryDeleteForEveryone: action('retryDeleteForEveryone'),
retrySend: action('retrySend'),
deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
showMessageDetail: action('showMessageDetail'),
openConversation: action('openConversation'),
2022-12-14 18:12:04 +00:00
saveAttachment: action('saveAttachment'),
showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'),
2021-01-29 22:58:28 +00:00
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
markViewed: action('markViewed'),
messageExpanded: action('messageExpanded'),
2022-12-10 02:02:22 +00:00
showLightbox: action('showLightbox'),
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
openLink: action('openLink'),
2022-05-11 20:59:58 +00:00
openGiftBadge: action('openGiftBadge'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast'
),
toggleForwardMessageModal: action('toggleForwardMessageModal'),
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
downloadNewVersion: action('downloadNewVersion'),
startCallingLobby: action('startCallingLobby'),
startConversation: action('startConversation'),
returnToActiveCall: action('returnToActiveCall'),
2021-02-18 16:40:26 +00:00
contactSupport: action('contactSupport'),
2021-04-21 16:31:12 +00:00
closeContactSpoofingReview: action('closeContactSpoofingReview'),
2021-06-01 23:30:25 +00:00
reviewGroupMemberNameCollision: action('reviewGroupMemberNameCollision'),
2021-04-21 16:31:12 +00:00
reviewMessageRequestNameCollision: action(
'reviewMessageRequestNameCollision'
),
2022-12-06 19:03:09 +00:00
acceptConversation: action('acceptConversation'),
blockAndReportSpam: action('blockAndReportSpam'),
blockConversation: action('blockConversation'),
deleteConversation: action('deleteConversation'),
2021-06-01 23:30:25 +00:00
removeMember: action('removeMember'),
unblurAvatar: action('unblurAvatar'),
peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'),
peekGroupCallIfItHasMembers: action('peekGroupCallIfItHasMembers'),
2022-07-06 19:06:20 +00:00
viewStory: action('viewStory'),
});
const renderItem = ({
messageId,
containerElementRef,
containerWidthBreakpoint,
}: {
messageId: string;
containerElementRef: React.RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
}) => (
<TimelineItem
2021-11-17 21:11:46 +00:00
getPreferredBadge={() => undefined}
id=""
isSelected={false}
renderEmojiPicker={() => <div />}
renderReactionPicker={() => <div />}
item={items[messageId]}
i18n={i18n}
interactionMode="keyboard"
isNextItemCallingNotification={false}
theme={ThemeType.light}
containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint}
conversationId=""
2020-09-09 02:25:05 +00:00
renderContact={() => '*ContactName*'}
2021-06-01 20:45:43 +00:00
renderUniversalTimerNotification={() => (
<div>*UniversalTimerNotification*</div>
)}
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
shouldCollapseAbove={false}
shouldCollapseBelow={false}
shouldHideMetadata={false}
shouldRenderDateHeader={false}
{...actions()}
/>
);
const renderContactSpoofingReviewDialog = (
props: SmartContactSpoofingReviewDialogPropsType
) => {
if (props.type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
return (
<ContactSpoofingReviewDialog
{...props}
group={{
...getDefaultConversation(),
areWeAdmin: true,
}}
/>
);
}
return <ContactSpoofingReviewDialog {...props} />;
};
const getAbout = () => text('about', '👍 Free to chat');
const getTitle = () => text('name', 'Cayce Bollard');
const getProfileName = () => text('profileName', 'Cayce Bollard (profile)');
const getAvatarPath = () =>
text('avatarPath', '/fixtures/kitten-4-112-112.jpg');
const getPhoneNumber = () => text('phoneNumber', '+1 (808) 555-1234');
2021-11-02 23:01:13 +00:00
const renderHeroRow = () => {
2022-11-18 00:45:19 +00:00
function Wrapper() {
2021-11-02 23:01:13 +00:00
const theme = React.useContext(StorybookThemeContext);
return (
<ConversationHero
about={getAbout()}
acceptedMessageRequest
2022-07-22 00:44:35 +00:00
avatarPath={getAvatarPath()}
badge={undefined}
2022-07-22 00:44:35 +00:00
conversationType="direct"
id={getDefaultConversation().id}
2021-11-02 23:01:13 +00:00
i18n={i18n}
isMe={false}
phoneNumber={getPhoneNumber()}
2022-07-22 00:44:35 +00:00
profileName={getProfileName()}
2021-11-02 23:01:13 +00:00
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
theme={theme}
2022-07-22 00:44:35 +00:00
title={getTitle()}
2021-11-02 23:01:13 +00:00
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={noop}
2022-07-22 00:44:35 +00:00
viewUserStories={action('viewUserStories')}
2021-11-02 23:01:13 +00:00
/>
);
2022-11-18 00:45:19 +00:00
}
2021-11-02 23:01:13 +00:00
return <Wrapper />;
};
const renderTypingBubble = () => (
<TypingBubble
2021-05-07 22:21:10 +00:00
acceptedMessageRequest
badge={undefined}
2021-08-06 00:17:05 +00:00
color={getRandomColor()}
conversationType="direct"
phoneNumber="+18005552222"
i18n={i18n}
2021-05-07 22:21:10 +00:00
isMe={false}
title="title"
theme={ThemeType.light}
2021-05-07 22:21:10 +00:00
sharedGroupNames={[]}
/>
);
2021-11-20 15:41:21 +00:00
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
discardMessages: action('discardMessages'),
2021-11-20 15:41:21 +00:00
getPreferredBadge: () => undefined,
i18n,
2021-11-20 15:41:21 +00:00
theme: React.useContext(StorybookThemeContext),
2022-01-26 23:05:26 +00:00
getTimestampForMessage: Date.now,
haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false),
haveOldest: boolean('haveOldest', overrideProps.haveOldest !== false),
isConversationSelected: true,
isIncomingMessageRequest: boolean(
'isIncomingMessageRequest',
overrideProps.isIncomingMessageRequest === true
),
items: overrideProps.items || Object.keys(items),
messageChangeCounter: 0,
scrollToIndex: overrideProps.scrollToIndex,
scrollToIndexCounter: 0,
totalUnseen: number('totalUnseen', overrideProps.totalUnseen || 0),
oldestUnseenIndex:
number('oldestUnseenIndex', overrideProps.oldestUnseenIndex || 0) ||
undefined,
2021-03-03 20:09:58 +00:00
invitedContactsForNewlyCreatedGroup:
overrideProps.invitedContactsForNewlyCreatedGroup || [],
2021-04-21 16:31:12 +00:00
warning: overrideProps.warning,
2021-06-01 23:30:25 +00:00
id: uuid(),
renderItem,
renderHeroRow,
renderTypingBubble,
renderContactSpoofingReviewDialog,
isSomeoneTyping: overrideProps.isSomeoneTyping || false,
...actions(),
});
2022-11-18 00:45:19 +00:00
export function OldestAndNewest(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps();
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-06-07 00:48:02 +00:00
OldestAndNewest.story = {
name: 'Oldest and Newest',
};
2022-11-18 00:45:19 +00:00
export function WithActiveMessageRequest(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
isIncomingMessageRequest: true,
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-06-07 00:48:02 +00:00
WithActiveMessageRequest.story = {
name: 'With active message request',
};
2022-11-18 00:45:19 +00:00
export function WithoutNewestMessage(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
haveNewest: false,
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-11-18 00:45:19 +00:00
export function WithoutNewestMessageActiveMessageRequest(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
haveOldest: false,
isIncomingMessageRequest: true,
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-06-07 00:48:02 +00:00
WithoutNewestMessageActiveMessageRequest.story = {
name: 'Without newest message, active message request',
};
2022-11-18 00:45:19 +00:00
export function WithoutOldestMessage(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
haveOldest: false,
scrollToIndex: -1,
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-11-18 00:45:19 +00:00
export function EmptyJustHero(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
items: [],
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-06-07 00:48:02 +00:00
EmptyJustHero.story = {
name: 'Empty (just hero)',
};
2022-11-18 00:45:19 +00:00
export function LastSeen(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
oldestUnseenIndex: 13,
totalUnseen: 2,
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-11-18 00:45:19 +00:00
export function TargetIndexToTop(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
scrollToIndex: 0,
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-06-07 00:48:02 +00:00
TargetIndexToTop.story = {
name: 'Target Index to Top',
};
2022-11-18 00:45:19 +00:00
export function TypingIndicator(): JSX.Element {
const props = useProps({ isSomeoneTyping: true });
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2021-03-03 20:09:58 +00:00
2022-11-18 00:45:19 +00:00
export function WithInvitedContactsForANewlyCreatedGroup(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
2021-03-03 20:09:58 +00:00
invitedContactsForNewlyCreatedGroup: [
2021-05-07 22:21:10 +00:00
getDefaultConversation({
2021-03-03 20:09:58 +00:00
id: 'abc123',
title: 'John Bon Bon Jovi',
2021-05-07 22:21:10 +00:00
}),
getDefaultConversation({
2021-03-03 20:09:58 +00:00
id: 'def456',
title: 'Bon John Bon Jovi',
2021-05-07 22:21:10 +00:00
}),
2021-03-03 20:09:58 +00:00
],
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2021-04-21 16:31:12 +00:00
2022-06-07 00:48:02 +00:00
WithInvitedContactsForANewlyCreatedGroup.story = {
name: 'With invited contacts for a newly-created group',
};
2022-11-18 00:45:19 +00:00
export function WithSameNameInDirectConversationWarning(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
2021-04-21 16:31:12 +00:00
warning: {
2021-06-01 23:30:25 +00:00
type: ContactSpoofingType.DirectConversationWithSameTitle,
2021-04-21 16:31:12 +00:00
safeConversation: getDefaultConversation(),
},
items: [],
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-06-07 00:48:02 +00:00
WithSameNameInDirectConversationWarning.story = {
name: 'With "same name in direct conversation" warning',
};
2021-06-01 23:30:25 +00:00
2022-11-18 00:45:19 +00:00
export function WithSameNameInGroupConversationWarning(): JSX.Element {
2021-11-20 15:41:21 +00:00
const props = useProps({
2021-06-01 23:30:25 +00:00
warning: {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
acknowledgedGroupNameCollisions: {},
groupNameCollisions: {
Alice: times(2, () => uuid()),
Bob: times(3, () => uuid()),
},
},
items: [],
});
return <Timeline {...props} />;
2022-11-18 00:45:19 +00:00
}
2022-06-07 00:48:02 +00:00
WithSameNameInGroupConversationWarning.story = {
name: 'With "same name in group conversation" warning',
};