Files
Desktop/ts/components/conversation/Quote.dom.stories.tsx
2026-02-13 14:07:18 -06:00

643 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Meta, StoryFn } from '@storybook/react';
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { ConversationColors } from '../../types/Colors.std.js';
import { pngUrl } from '../../storybook/Fixtures.std.js';
import type { Props as TimelineMessagesProps } from './TimelineMessage.dom.js';
import { TimelineMessage } from './TimelineMessage.dom.js';
import { MessageInteractivity, TextDirection } from './Message.dom.js';
import {
AUDIO_MP3,
IMAGE_PNG,
IMAGE_GIF,
LONG_MESSAGE,
VIDEO_MP4,
stringToMIMEType,
} from '../../types/MIME.std.js';
import type { Props } from './Quote.dom.js';
import { Quote } from './Quote.dom.js';
import { ReadStatus } from '../../messages/MessageReadStatus.std.js';
import { getDefaultConversation } from '../../test-helpers/getDefaultConversation.std.js';
import { WidthBreakpoint } from '../_util.std.js';
import { ThemeType } from '../../types/Util.std.js';
import { PaymentEventKind } from '../../types/Payment.std.js';
const { i18n } = window.SignalContext;
export default {
component: Quote,
title: 'Components/Conversation/Quote',
argTypes: {
isFromMe: {
control: { type: 'boolean' },
},
isGiftBadge: {
control: { type: 'boolean' },
},
isIncoming: {
control: { type: 'boolean' },
},
isViewOnce: {
control: { type: 'boolean' },
},
referencedMessageNotFound: {
control: { type: 'boolean' },
},
},
args: {
authorTitle: 'Default Sender',
conversationColor: 'forest',
doubleCheckMissingQuoteReference: action(
'doubleCheckMissingQuoteReference'
),
i18n,
isFromMe: false,
isGiftBadge: false,
isIncoming: false,
isViewOnce: false,
onClick: action('onClick'),
onClose: action('onClose'),
rawAttachment: undefined,
referencedMessageNotFound: false,
text: 'A sample message from a pal',
},
} satisfies Meta<Props>;
const defaultMessageProps: TimelineMessagesProps = {
author: getDefaultConversation({
id: 'some-id',
title: 'Person X',
}),
canCopy: true,
canEditMessage: true,
canEndPoll: false,
canForward: true,
canPinMessage: true,
canReact: true,
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
canDeleteForEveryone: true,
canDownload: true,
checkForAccount: action('checkForAccount'),
clearTargetedMessage: action('default--clearTargetedMessage'),
containerElementRef: React.createRef<HTMLElement>(),
containerWidthBreakpoint: WidthBreakpoint.Wide,
conversationColor: 'crimson',
conversationId: 'conversationId',
conversationTitle: 'Conversation Title',
conversationType: 'direct', // override
direction: 'incoming',
showLightboxForViewOnceMedia: action('default--showLightboxForViewOnceMedia'),
doubleCheckMissingQuoteReference: action(
'default--doubleCheckMissingQuoteReference'
),
getPreferredBadge: () => undefined,
i18n,
platform: 'darwin',
id: 'messageId',
// renderingContext: 'storybook',
interactivity: MessageInteractivity.Normal,
interactionMode: 'keyboard',
isBlocked: false,
isMessageRequestAccepted: true,
isPinned: false,
isSelected: false,
isSelectMode: false,
isSMS: false,
isSpoilerExpanded: {},
isVoiceMessagePlayed: false,
handleDebugMessage: action('debugMessage'),
toggleSelectMessage: action('toggleSelectMessage'),
cancelAttachmentDownload: action('default--cancelAttachmentDownload'),
kickOffAttachmentDownload: action('default--kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
messageExpanded: action('default--message-expanded'),
showConversation: action('default--showConversation'),
openGiftBadge: action('openGiftBadge'),
previews: [],
reactToMessage: action('default--reactToMessage'),
endPoll: action('default--endPoll'),
readStatus: ReadStatus.Read,
renderReactionPicker: () => <div />,
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
setMessageToEdit: action('setMessageToEdit'),
setQuoteByMessageId: action('default--setQuoteByMessageId'),
retryMessageSend: action('default--retryMessageSend'),
sendPollVote: action('default--sendPollVote'),
copyMessageText: action('copyMessageText'),
retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
saveAttachment: action('saveAttachment'),
saveAttachments: action('saveAttachments'),
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
targetMessage: action('default--targetMessage'),
shouldCollapseAbove: false,
shouldCollapseBelow: false,
shouldHideMetadata: false,
showSpoiler: action('showSpoiler'),
showPinMessageDialog: action('showPinMessageDialog'),
onPinnedMessageRemove: action('onPinnedMessageRemove'),
pushPanelForConversation: action('default--pushPanelForConversation'),
showContactModal: action('default--showContactModal'),
showAttachmentDownloadStillInProgressToast: action(
'showAttachmentDownloadStillInProgressToast'
),
showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast'
),
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
showTapToViewNotAvailableModal: action('showTapToViewNotAvailableModal'),
toggleDeleteMessagesModal: action('default--toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('default--toggleForwardMessagesModal'),
showLightbox: action('default--showLightbox'),
startConversation: action('default--startConversation'),
status: 'sent',
text: 'This is really interesting.',
textDirection: TextDirection.Default,
theme: ThemeType.light,
timestamp: Date.now(),
viewStory: action('viewStory'),
};
const renderInMessage = ({
authorTitle,
authorLabel,
conversationColor,
isFromMe,
rawAttachment,
isViewOnce,
isGiftBadge,
referencedMessageNotFound,
text: quoteText,
}: Props) => {
const messageProps = {
...defaultMessageProps,
conversationColor,
quote: {
authorId: 'an-author',
authorTitle,
authorLabel,
conversationColor,
conversationTitle: getDefaultConversation().title,
isFromMe,
rawAttachment,
isViewOnce,
isGiftBadge,
referencedMessageNotFound,
sentAt: Date.now() - 30 * 1000,
text: quoteText,
},
};
return (
<div style={{ overflow: 'hidden' }}>
<TimelineMessage {...messageProps} />
<br />
<TimelineMessage {...messageProps} direction="outgoing" />
</div>
);
};
// eslint-disable-next-line react/function-component-definition
const Template: StoryFn<Props> = args => <Quote {...args} />;
const TemplateInMessage: StoryFn<Props> = args => renderInMessage(args);
export const OutgoingByAnotherAuthor = Template.bind({});
OutgoingByAnotherAuthor.args = {
authorTitle: getDefaultConversation().title,
};
export const OutgoingByMe = Template.bind({});
OutgoingByMe.args = {
isFromMe: true,
};
export const IncomingByAnotherAuthor = Template.bind({});
IncomingByAnotherAuthor.args = {
authorTitle: getDefaultConversation().title,
isIncoming: true,
};
export const IncomingByAnotherWithLabel = Template.bind({});
IncomingByAnotherWithLabel.args = {
authorTitle: getDefaultConversation().title,
isIncoming: true,
authorLabel: {
labelEmoji: '1⃣',
labelString: 'First',
},
};
export const IncomingByMe = Template.bind({});
IncomingByMe.args = {
isFromMe: true,
isIncoming: true,
};
export function IncomingOutgoingColors(args: Props): React.JSX.Element {
return (
<>
{ConversationColors.map(color =>
renderInMessage({
...args,
conversationColor: color,
authorLabel: {
labelEmoji: '1⃣',
labelString: 'First',
},
})
)}
</>
);
}
IncomingOutgoingColors.args = {};
export const ImageOnly = Template.bind({});
ImageOnly.args = {
text: '',
rawAttachment: {
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
size: 100,
path: pngUrl,
url: pngUrl,
},
},
};
export const ImageAttachment = Template.bind({});
ImageAttachment.args = {
rawAttachment: {
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
size: 100,
path: pngUrl,
url: pngUrl,
},
},
};
export const ImageAttachmentUndownloaded = Template.bind({});
ImageAttachmentUndownloaded.args = {
rawAttachment: {
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: undefined,
size: 100000,
},
},
};
export const ImageAttachmentDownloading = Template.bind({});
ImageAttachmentDownloading.args = {
rawAttachment: {
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: undefined,
pending: true,
size: 100000,
totalDownloaded: 75000,
},
},
};
export const ImageAttachmentNoThumbnail = Template.bind({});
ImageAttachmentNoThumbnail.args = {
rawAttachment: {
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
},
};
export const ImageTapToView = Template.bind({});
ImageTapToView.args = {
text: '',
isViewOnce: true,
rawAttachment: {
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
},
};
export const VideoOnly = Template.bind({});
VideoOnly.args = {
rawAttachment: {
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
size: 100,
path: pngUrl,
url: pngUrl,
},
},
text: undefined,
};
export const VideoAttachment = Template.bind({});
VideoAttachment.args = {
rawAttachment: {
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
size: 100,
path: pngUrl,
url: pngUrl,
},
},
};
export const VideoAttachmentUndownloaded = Template.bind({});
VideoAttachmentUndownloaded.args = {
rawAttachment: {
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: undefined,
size: 100000,
},
},
};
export const VideoAttachmentDownloading = Template.bind({});
VideoAttachmentDownloading.args = {
rawAttachment: {
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: undefined,
pending: true,
size: 100000,
totalDownloaded: 75000,
},
},
};
export const VideoAttachmentNoThumbnail = Template.bind({});
VideoAttachmentNoThumbnail.args = {
rawAttachment: {
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
},
};
export const VideoTapToView = Template.bind({});
VideoTapToView.args = {
text: '',
isViewOnce: true,
rawAttachment: {
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
},
};
export const GiftBadge = TemplateInMessage.bind({});
GiftBadge.args = {
text: "Some text which shouldn't be rendered",
isGiftBadge: true,
};
export const AudioOnly = Template.bind({});
AudioOnly.args = {
rawAttachment: {
contentType: AUDIO_MP3,
fileName: 'great-video.mp3',
isVoiceMessage: false,
},
text: undefined,
};
export const AudioAttachment = Template.bind({});
AudioAttachment.args = {
rawAttachment: {
contentType: AUDIO_MP3,
fileName: 'great-video.mp3',
isVoiceMessage: false,
},
};
export const VoiceMessageOnly = Template.bind({});
VoiceMessageOnly.args = {
rawAttachment: {
contentType: AUDIO_MP3,
fileName: 'great-video.mp3',
isVoiceMessage: true,
},
text: undefined,
};
export const VoiceMessageAttachment = Template.bind({});
VoiceMessageAttachment.args = {
rawAttachment: {
contentType: AUDIO_MP3,
fileName: 'great-video.mp3',
isVoiceMessage: true,
},
};
export const GIFAttachmentOnly = Template.bind({});
GIFAttachmentOnly.args = {
rawAttachment: {
contentType: IMAGE_GIF,
fileName: 'sax.png',
},
text: undefined,
};
export const OtherFileOnly = Template.bind({});
OtherFileOnly.args = {
rawAttachment: {
contentType: stringToMIMEType('application/json'),
fileName: 'great-data.json',
isVoiceMessage: false,
},
text: undefined,
};
export const MediaTapToView = Template.bind({});
MediaTapToView.args = {
text: '',
isViewOnce: true,
rawAttachment: {
contentType: AUDIO_MP3,
fileName: 'great-video.mp3',
isVoiceMessage: false,
},
};
export const OtherFileAttachment = Template.bind({});
OtherFileAttachment.args = {
rawAttachment: {
contentType: stringToMIMEType('application/json'),
fileName: 'great-data.json',
isVoiceMessage: false,
},
};
export const LongMessageAttachmentShouldBeHidden = Template.bind({});
LongMessageAttachmentShouldBeHidden.args = {
rawAttachment: {
contentType: LONG_MESSAGE,
fileName: 'signal-long-message-123.txt',
isVoiceMessage: false,
},
};
export const NoCloseButton = Template.bind({});
NoCloseButton.args = {
onClose: undefined,
};
export const MessageNotFound = TemplateInMessage.bind({});
MessageNotFound.args = {
referencedMessageNotFound: true,
};
export const MissingTextAttachment = Template.bind({});
MissingTextAttachment.args = {
text: undefined,
};
export const MentionOutgoingAnotherAuthor = Template.bind({});
MentionOutgoingAnotherAuthor.args = {
authorTitle: 'Tony Stark',
text: '@Captain America Lunch later?',
};
export const MentionOutgoingMe = Template.bind({});
MentionOutgoingMe.args = {
isFromMe: true,
text: '@Captain America Lunch later?',
};
export const MentionIncomingAnotherAuthor = Template.bind({});
MentionIncomingAnotherAuthor.args = {
authorTitle: 'Captain America',
isIncoming: true,
text: '@Tony Stark sure',
};
export const MentionIncomingMe = Template.bind({});
MentionIncomingMe.args = {
isFromMe: true,
isIncoming: true,
text: '@Tony Stark sure',
};
export function CustomColor(args: Props): React.JSX.Element {
return (
<>
<Quote
{...args}
customColor={{
start: { hue: 82, saturation: 35 },
}}
/>
<Quote
{...args}
isIncoming={false}
text="A gradient"
customColor={{
deg: 192,
start: { hue: 304, saturation: 85 },
end: { hue: 231, saturation: 76 },
}}
/>
</>
);
}
CustomColor.args = {
isIncoming: true,
text: 'Solid + Gradient',
};
export const IsStoryReply = Template.bind({});
IsStoryReply.args = {
text: 'Wow!',
authorTitle: 'Amanda',
isStoryReply: true,
moduleClassName: 'StoryReplyQuote',
onClose: undefined,
rawAttachment: {
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
},
};
export const IsStoryReplyEmoji = Template.bind({});
IsStoryReplyEmoji.args = {
authorTitle: getDefaultConversation().firstName,
isStoryReply: true,
moduleClassName: 'StoryReplyQuote',
onClose: undefined,
rawAttachment: {
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
size: 100,
path: pngUrl,
url: pngUrl,
},
},
reactionEmoji: '🏋️',
};
export const Payment = Template.bind({});
Payment.args = {
text: '',
payment: {
kind: PaymentEventKind.Notification,
note: null,
},
};