mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-17 23:34:14 +01:00
643 lines
16 KiB
TypeScript
643 lines
16 KiB
TypeScript
// 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,
|
||
},
|
||
};
|