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

616 lines
17 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 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 { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';
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';
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',
argTypes: {},
args: {},
} satisfies Meta<PropsType>;
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({}),
canCopy: true,
2021-06-02 17:27:12 +00:00
canDeleteForEveryone: false,
canDownload: true,
canEditMessage: 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,
2023-03-20 22:23:53 +00:00
isSelected: false,
isSelectMode: false,
isSpoilerExpanded: {},
2021-06-02 17:27:12 +00:00
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'),
2023-08-16 20:54:39 +00:00
clearInvitedServiceIdsForNewlyCreatedGroup: action(
'clearInvitedServiceIdsForNewlyCreatedGroup'
2021-03-03 20:09:58 +00:00
),
setIsNearBottom: action('setIsNearBottom'),
loadOlderMessages: action('loadOlderMessages'),
loadNewerMessages: action('loadNewerMessages'),
loadNewestMessages: action('loadNewestMessages'),
markMessageRead: action('markMessageRead'),
2023-03-20 22:23:53 +00:00
toggleSelectMessage: action('toggleSelectMessage'),
targetMessage: action('targetMessage'),
scrollToOldestUnreadMention: action('scrollToOldestUnreadMention'),
2023-03-20 22:23:53 +00:00
clearTargetedMessage: action('clearTargetedMessage'),
updateSharedGroups: action('updateSharedGroups'),
reactToMessage: action('reactToMessage'),
setMessageToEdit: action('setMessageToEdit'),
setQuoteByMessageId: action('setQuoteByMessageId'),
copyMessageText: action('copyMessageText'),
retryDeleteForEveryone: action('retryDeleteForEveryone'),
retryMessageSend: action('retryMessageSend'),
2022-12-14 18:12:04 +00:00
saveAttachment: action('saveAttachment'),
pushPanelForConversation: action('pushPanelForConversation'),
showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'),
showConversation: action('showConversation'),
2021-01-29 22:58:28 +00:00
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
messageExpanded: action('messageExpanded'),
showSpoiler: action('showSpoiler'),
2022-12-10 02:02:22 +00:00
showLightbox: action('showLightbox'),
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
2022-05-11 20:59:58 +00:00
openGiftBadge: action('openGiftBadge'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast'
),
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
2023-03-20 22:23:53 +00:00
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation'
),
onOutgoingVideoCallInConversation: action(
'onOutgoingVideoCallInConversation'
),
startConversation: action('startConversation'),
returnToActiveCall: action('returnToActiveCall'),
2021-02-18 16:40:26 +00:00
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'
),
unblurAvatar: action('unblurAvatar'),
peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'),
peekGroupCallIfItHasMembers: action('peekGroupCallIfItHasMembers'),
2022-07-06 19:06:20 +00:00
viewStory: action('viewStory'),
2023-03-20 22:23:53 +00:00
onReplyToMessage: action('onReplyToMessage'),
});
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=""
2023-03-20 22:23:53 +00:00
isTargeted={false}
i18n={i18n}
interactionMode="keyboard"
isNextItemCallingNotification={false}
theme={ThemeType.light}
2023-04-03 20:16:27 +00:00
platform="darwin"
containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint}
conversationId=""
item={items[messageId]}
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
2020-09-09 02:25:05 +00:00
renderContact={() => '*ContactName*'}
renderEmojiPicker={() => <div />}
renderReactionPicker={() => <div />}
2021-06-01 20:45:43 +00:00
renderUniversalTimerNotification={() => (
<div>*UniversalTimerNotification*</div>
)}
shouldCollapseAbove={false}
shouldCollapseBelow={false}
shouldHideMetadata={false}
shouldRenderDateHeader={false}
{...actions()}
/>
);
const renderContactSpoofingReviewDialog = (
props: SmartContactSpoofingReviewDialogPropsType
) => {
const sharedProps = {
acceptConversation: action('acceptConversation'),
blockAndReportSpam: action('blockAndReportSpam'),
blockConversation: action('blockConversation'),
deleteConversation: action('deleteConversation'),
getPreferredBadge: () => undefined,
i18n,
removeMember: action('removeMember'),
showContactModal: action('showContactModal'),
theme: ThemeType.dark,
};
if (props.type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
return (
<ContactSpoofingReviewDialog
{...props}
{...sharedProps}
group={{
...getDefaultConversation(),
areWeAdmin: true,
}}
/>
);
}
return <ContactSpoofingReviewDialog {...props} {...sharedProps} />;
};
const getAbout = () => '👍 Free to chat';
const getTitle = () => 'Cayce Bollard';
const getProfileName = () => 'Cayce Bollard (profile)';
const getAvatarPath = () => '/fixtures/kitten-4-112-112.jpg';
const getPhoneNumber = () => '+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
2023-09-27 21:23:52 +00:00
typingContactIdTimestamps={{ [getDefaultConversation().id]: Date.now() }}
lastItemAuthorId="123"
lastItemTimestamp={undefined}
conversationId="123"
conversationType="direct"
2023-09-27 21:23:52 +00:00
getConversation={() => getDefaultConversation()}
getPreferredBadge={() => undefined}
showContactModal={action('showContactModal')}
i18n={i18n}
theme={ThemeType.light}
/>
);
const renderMiniPlayer = () => (
<div>If active, this is where smart mini player would be</div>
);
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: overrideProps.haveNewest ?? false,
haveOldest: overrideProps.haveOldest ?? false,
isConversationSelected: true,
isIncomingMessageRequest: overrideProps.isIncomingMessageRequest ?? false,
items: overrideProps.items ?? Object.keys(items),
messageChangeCounter: 0,
scrollToIndex: overrideProps.scrollToIndex,
scrollToIndexCounter: 0,
shouldShowMiniPlayer: Boolean(overrideProps.shouldShowMiniPlayer),
totalUnseen: overrideProps.totalUnseen ?? 0,
oldestUnseenIndex: overrideProps.oldestUnseenIndex ?? 0,
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,
renderMiniPlayer,
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-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
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-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
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
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-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
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
export function WithJustMiniPlayer(): JSX.Element {
const props = useProps({
shouldShowMiniPlayer: true,
items: [],
});
return <Timeline {...props} />;
}