signal-desktop/ts/components/conversation/TimelineItem.stories.tsx
Fedor Indutny 12d7f24d0f New UI for audio playback and global audio player
Introduce new UI and behavior for playing audio attachments in
conversations. Previously, playback stopped unexpectedly during window
resizes and scrolling through the messages due to the row height
recomputation in `react-virtualized`.

With this commit we introduce `<GlobalAudioContext/>` instance that
wraps whole conversation and provides an `<audio/>` element that
doesn't get re-rendered (or destroyed) whenever `react-virtualized`
recomputes messages. The audio players (with a freshly designed UI) now
share this global `<audio/>` instance and manage access to it using
`audioPlayer.owner` state from the redux.

New UI computes on the fly, caches, and displays waveforms for each
audio attachment. Storybook had to be slightly modified to accomodate
testing of Android bubbles by introducing the new knob for
`authorColor`.
2021-03-19 16:57:35 -04:00

389 lines
10 KiB
TypeScript

// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { EmojiPicker } from '../emoji/EmojiPicker';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
import { CallMode } from '../../types/Calling';
const i18n = setupI18n('en', enMessages);
const renderEmojiPicker: TimelineItemProps['renderEmojiPicker'] = ({
onClose,
onPickEmoji,
ref,
}) => (
<EmojiPicker
i18n={setupI18n('en', enMessages)}
skinTone={0}
onSetSkinTone={action('EmojiPicker::onSetSkinTone')}
ref={ref}
onClose={onClose}
onPickEmoji={onPickEmoji}
/>
);
const renderContact = (conversationId: string) => (
<React.Fragment key={conversationId}>{conversationId}</React.Fragment>
);
const getDefaultProps = () => ({
conversationId: 'conversation-id',
conversationAccepted: true,
id: 'asdf',
isSelected: false,
selectMessage: action('selectMessage'),
reactToMessage: action('reactToMessage'),
clearSelectedMessage: action('clearSelectedMessage'),
contactSupport: action('contactSupport'),
replyToMessage: action('replyToMessage'),
retrySend: action('retrySend'),
deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
showMessageDetail: action('showMessageDetail'),
openConversation: action('openConversation'),
showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'),
showVisualAttachment: action('showVisualAttachment'),
downloadAttachment: action('downloadAttachment'),
displayTapToViewMessage: action('displayTapToViewMessage'),
showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
showExpiredOutgoingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
openLink: action('openLink'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
downloadNewVersion: action('downloadNewVersion'),
showIdentity: action('showIdentity'),
messageSizeChanged: action('messageSizeChanged'),
startCallingLobby: action('startCallingLobby'),
returnToActiveCall: action('returnToActiveCall'),
renderContact,
renderEmojiPicker,
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
});
storiesOf('Components/Conversation/TimelineItem', module)
.add('Plain Message', () => {
const item = {
type: 'message',
data: {
id: 'id-1',
direction: 'incoming',
timestamp: Date.now(),
authorPhoneNumber: '(202) 555-2001',
authorColor: 'green',
text: '🔥',
},
} as TimelineItemProps['item'];
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
})
.add('Notification', () => {
const items = [
{
type: 'timerNotification',
data: {
type: 'fromOther',
phoneNumber: '(202) 555-0000',
timespan: '1 hour',
},
},
{
type: 'callHistory',
data: {
// declined incoming audio
callMode: CallMode.Direct,
wasDeclined: true,
wasIncoming: true,
wasVideoCall: false,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// declined incoming video
callMode: CallMode.Direct,
wasDeclined: true,
wasIncoming: true,
wasVideoCall: true,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// accepted incoming audio
callMode: CallMode.Direct,
acceptedTime: Date.now() - 300,
wasDeclined: false,
wasIncoming: true,
wasVideoCall: false,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// accepted incoming video
callMode: CallMode.Direct,
acceptedTime: Date.now() - 400,
wasDeclined: false,
wasIncoming: true,
wasVideoCall: true,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// missed (neither accepted nor declined) incoming audio
callMode: CallMode.Direct,
wasDeclined: false,
wasIncoming: true,
wasVideoCall: false,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// missed (neither accepted nor declined) incoming video
callMode: CallMode.Direct,
wasDeclined: false,
wasIncoming: true,
wasVideoCall: true,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// accepted outgoing audio
callMode: CallMode.Direct,
acceptedTime: Date.now() - 200,
wasDeclined: false,
wasIncoming: false,
wasVideoCall: false,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// accepted outgoing video
callMode: CallMode.Direct,
acceptedTime: Date.now() - 200,
wasDeclined: false,
wasIncoming: false,
wasVideoCall: true,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// declined outgoing audio
callMode: CallMode.Direct,
wasDeclined: true,
wasIncoming: false,
wasVideoCall: false,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// declined outgoing video
callMode: CallMode.Direct,
wasDeclined: true,
wasIncoming: false,
wasVideoCall: true,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// missed (neither accepted nor declined) outgoing audio
callMode: CallMode.Direct,
wasDeclined: false,
wasIncoming: false,
wasVideoCall: false,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// missed (neither accepted nor declined) outgoing video
callMode: CallMode.Direct,
wasDeclined: false,
wasIncoming: false,
wasVideoCall: true,
endedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// ongoing group call
callMode: CallMode.Group,
conversationId: 'abc123',
creator: {
firstName: 'Luigi',
isMe: false,
title: 'Luigi Mario',
},
ended: false,
deviceCount: 1,
maxDevices: 16,
startedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// ongoing group call started by you
callMode: CallMode.Group,
conversationId: 'abc123',
creator: {
firstName: 'Peach',
isMe: true,
title: 'Princess Peach',
},
ended: false,
deviceCount: 1,
maxDevices: 16,
startedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// ongoing group call, creator unknown
callMode: CallMode.Group,
conversationId: 'abc123',
ended: false,
deviceCount: 1,
maxDevices: 16,
startedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// ongoing and active group call
callMode: CallMode.Group,
activeCallConversationId: 'abc123',
conversationId: 'abc123',
creator: {
firstName: 'Luigi',
isMe: false,
title: 'Luigi Mario',
},
ended: false,
deviceCount: 1,
maxDevices: 16,
startedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// ongoing group call, but you're in another one
callMode: CallMode.Group,
activeCallConversationId: 'abc123',
conversationId: 'xyz987',
creator: {
firstName: 'Luigi',
isMe: false,
title: 'Luigi Mario',
},
ended: false,
deviceCount: 1,
maxDevices: 16,
startedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// ongoing full group call
callMode: CallMode.Group,
conversationId: 'abc123',
creator: {
firstName: 'Luigi',
isMe: false,
title: 'Luigi Mario',
},
ended: false,
deviceCount: 16,
maxDevices: 16,
startedTime: Date.now(),
},
},
{
type: 'callHistory',
data: {
// finished call
callMode: CallMode.Group,
conversationId: 'abc123',
creator: {
firstName: 'Luigi',
isMe: false,
title: 'Luigi Mario',
},
ended: true,
deviceCount: 0,
maxDevices: 16,
startedTime: Date.now(),
},
},
];
return (
<>
{items.map((item, index) => (
<React.Fragment key={index}>
<TimelineItem
{...getDefaultProps()}
item={item as TimelineItemProps['item']}
i18n={i18n}
/>
<hr />
</React.Fragment>
))}
</>
);
})
.add('Unknown Type', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: intentional
const item = {
type: 'random',
data: {
somethin: 'somethin',
},
} as TimelineItemProps['item'];
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
})
.add('Missing Item', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: intentional
const item = null as TimelineItemProps['item'];
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
});