@linktr.ee/messaging-react 4.2.0-rc-1787325521 → 4.2.0-rc-1787599724
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Card-BYJyA83X.js → Card-D7UPq5-g.js} +2 -2
- package/dist/{Card-BYJyA83X.js.map → Card-D7UPq5-g.js.map} +1 -1
- package/dist/{Card-BplJtZao.cjs → Card-DcBZqMBt.cjs} +2 -2
- package/dist/{Card-BplJtZao.cjs.map → Card-DcBZqMBt.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-BUC9vqhJ.js → index-CSkr_CO5.js} +2089 -1952
- package/dist/index-CSkr_CO5.js.map +1 -0
- package/dist/index-CwkY7QwT.cjs +3 -0
- package/dist/index-CwkY7QwT.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +59 -78
- package/dist/index.js +26 -30
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/package.json +13 -11
- package/src/components/AttachmentCard/utils/icons.ts +7 -6
- package/src/components/Avatar/Avatar.test.tsx +0 -126
- package/src/components/Avatar/getAvatarEmoji.test.ts +0 -39
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +3 -410
- package/src/components/ChannelList/index.test.tsx +1 -132
- package/src/components/ChannelView.stories.tsx +0 -4
- package/src/components/ChannelView.test.tsx +3 -9
- package/src/components/CustomMessage/CustomMessage.stories.tsx +314 -6
- package/src/components/CustomMessage/CustomMessage.test.tsx +2 -56
- package/src/components/CustomMessage/MessageTag.stories.tsx +53 -17
- package/src/components/CustomMessage/MessageTag.test.tsx +40 -7
- package/src/components/CustomMessage/MessageTag.tsx +39 -7
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +7 -8
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +1 -2
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +12 -5
- package/src/components/CustomMessage/index.tsx +19 -22
- package/src/components/CustomMessageInput/index.tsx +1 -1
- package/src/components/CustomSystemMessage/index.tsx +2 -3
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +0 -327
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +165 -58
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.test.tsx +153 -0
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +450 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.test.ts +24 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.ts +16 -0
- package/src/components/MessageAttachment/Audio/index.tsx +83 -77
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +176 -74
- package/src/components/MessageAttachment/_shared/fileMeta.test.ts +0 -44
- package/src/components/MessageAttachment/_shared/useIsClient.ts +23 -0
- package/src/components/MessageAttachment/index.tsx +25 -9
- package/src/components/MessageAttachment/types.ts +19 -9
- package/src/components/MessagingShell/MessagingShell.test.tsx +0 -20
- package/src/components/MessagingShell/index.tsx +0 -2
- package/src/components/RichCard/RichCard.test.tsx +1 -3
- package/src/index.ts +0 -4
- package/src/providers/MessagingProvider.test.tsx +2 -130
- package/src/providers/MessagingProvider.tsx +15 -26
- package/src/styles.css +210 -57
- package/src/testing/createMockMessagingClient.ts +2 -2
- package/src/types.ts +0 -9
- package/dist/index-BUC9vqhJ.js.map +0 -1
- package/dist/index-BkqyKPJP.cjs +0 -3
- package/dist/index-BkqyKPJP.cjs.map +0 -1
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +0 -346
- package/src/components/AttachmentCard/Thumbnail.test.tsx +0 -215
- package/src/components/AttachmentCard/index.test.tsx +0 -160
- package/src/components/ChannelList/ChannelListContext.test.tsx +0 -87
- package/src/components/CustomMessage/Locked.stories.tsx +0 -108
- package/src/components/CustomMessage/PaidMessage.stories.tsx +0 -42
- package/src/components/CustomMessage/Tip.stories.tsx +0 -48
- package/src/components/CustomMessage/TipMessage/TipMessage.stories.tsx +0 -51
- package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +0 -65
- package/src/components/CustomMessage/TipMessage/index.tsx +0 -27
- package/src/components/FaqList/FaqList.test.tsx +0 -204
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +0 -76
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +0 -248
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +0 -123
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +0 -193
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +0 -242
- package/src/components/LinkAttachment/components/_shared/hex.test.ts +0 -37
- package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +0 -72
- package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +0 -162
- package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +0 -182
- package/src/components/MessageAttachment/_shared/Bubble.test.tsx +0 -111
- package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +0 -36
- package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +0 -114
- package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +0 -42
- package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +0 -168
- package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +0 -52
- package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +0 -69
- package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +0 -45
- package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +0 -55
- package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +0 -246
- package/src/components/MessageAttachment/_shared/triggerDownload.test.ts +0 -113
- package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +0 -68
- package/src/components/MessageAttachment/_shared/viewerChromeClasses.test.ts +0 -16
- package/src/logging/index.test.tsx +0 -136
- package/src/logging/index.tsx +0 -48
- package/src/stories/customMessageStoryTemplate.tsx +0 -202
|
@@ -11,25 +11,16 @@ const {
|
|
|
11
11
|
ChannelListProviderMock,
|
|
12
12
|
CustomChannelPreviewMock,
|
|
13
13
|
InfiniteScrollMock,
|
|
14
|
-
restorePendingMessagesMock,
|
|
15
|
-
streamMountMock,
|
|
16
|
-
messagingContextMock,
|
|
17
14
|
} = vi.hoisted(() => ({
|
|
18
15
|
ChannelListProviderMock: vi.fn(
|
|
19
16
|
({ children }: { children: React.ReactNode; value: unknown }) => children
|
|
20
17
|
),
|
|
21
18
|
CustomChannelPreviewMock: () => null,
|
|
22
19
|
InfiniteScrollMock: () => null,
|
|
23
|
-
restorePendingMessagesMock: vi.fn(),
|
|
24
|
-
streamMountMock: vi.fn(),
|
|
25
|
-
messagingContextMock: { current: { debug: false } },
|
|
26
20
|
}))
|
|
27
21
|
|
|
28
22
|
vi.mock('stream-chat-react', () => ({
|
|
29
23
|
ChannelList: (props: unknown) => {
|
|
30
|
-
React.useEffect(() => {
|
|
31
|
-
streamMountMock()
|
|
32
|
-
}, [])
|
|
33
24
|
streamChannelListMock(props)
|
|
34
25
|
return <div data-testid="stream-channel-list" />
|
|
35
26
|
},
|
|
@@ -37,11 +28,7 @@ vi.mock('stream-chat-react', () => ({
|
|
|
37
28
|
}))
|
|
38
29
|
|
|
39
30
|
vi.mock('../../providers/MessagingProvider', () => ({
|
|
40
|
-
useMessagingContext: () =>
|
|
41
|
-
}))
|
|
42
|
-
|
|
43
|
-
vi.mock('../../hooks/useRestorePendingMessages', () => ({
|
|
44
|
-
restorePendingMessages: restorePendingMessagesMock,
|
|
31
|
+
useMessagingContext: () => ({ debug: false }),
|
|
45
32
|
}))
|
|
46
33
|
|
|
47
34
|
vi.mock('./ChannelListContext', () => ({
|
|
@@ -61,9 +48,6 @@ describe('ChannelList', () => {
|
|
|
61
48
|
beforeEach(() => {
|
|
62
49
|
ChannelListProviderMock.mockClear()
|
|
63
50
|
streamChannelListMock.mockClear()
|
|
64
|
-
restorePendingMessagesMock.mockClear()
|
|
65
|
-
streamMountMock.mockClear()
|
|
66
|
-
messagingContextMock.current = { debug: false }
|
|
67
51
|
})
|
|
68
52
|
|
|
69
53
|
it('defaults unseen-channel promotion to false', () => {
|
|
@@ -216,119 +200,4 @@ describe('ChannelList', () => {
|
|
|
216
200
|
streamProps.channelRenderFilterFn!(mockChannels)
|
|
217
201
|
expect(filterFn).toHaveBeenCalledWith(mockChannels)
|
|
218
202
|
})
|
|
219
|
-
|
|
220
|
-
it('restores pending messages for every channel and preserves channels without a filter', () => {
|
|
221
|
-
const channels = [
|
|
222
|
-
{ cid: 'ch-1', state: { pending_messages: [] } },
|
|
223
|
-
{ cid: 'ch-2', state: { pending_messages: [{ id: 'pending-2' }] } },
|
|
224
|
-
] as unknown as Channel[]
|
|
225
|
-
|
|
226
|
-
renderWithProviders(<ChannelList {...defaultProps} />)
|
|
227
|
-
const streamProps = streamChannelListMock.mock.calls[0][0] as {
|
|
228
|
-
channelRenderFilterFn: (channels: Channel[]) => Channel[]
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
expect(streamProps.channelRenderFilterFn(channels)).toBe(channels)
|
|
232
|
-
expect(restorePendingMessagesMock).toHaveBeenCalledTimes(2)
|
|
233
|
-
expect(restorePendingMessagesMock).toHaveBeenNthCalledWith(1, channels[0])
|
|
234
|
-
expect(restorePendingMessagesMock).toHaveBeenNthCalledWith(2, channels[1])
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('remounts Stream ChannelList when filters change but not when they stay the same', () => {
|
|
238
|
-
const { rerender } = renderWithProviders(<ChannelList {...defaultProps} />)
|
|
239
|
-
|
|
240
|
-
expect(streamMountMock).toHaveBeenCalledOnce()
|
|
241
|
-
|
|
242
|
-
rerender(<ChannelList {...defaultProps} />)
|
|
243
|
-
expect(streamMountMock).toHaveBeenCalledOnce()
|
|
244
|
-
|
|
245
|
-
rerender(
|
|
246
|
-
<ChannelList
|
|
247
|
-
{...defaultProps}
|
|
248
|
-
filters={{ type: 'messaging', members: { $in: ['another-user'] } }}
|
|
249
|
-
/>
|
|
250
|
-
)
|
|
251
|
-
expect(streamMountMock).toHaveBeenCalledTimes(2)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('forwards the default and explicit sort values and the channel limit', () => {
|
|
255
|
-
renderWithProviders(<ChannelList {...defaultProps} />)
|
|
256
|
-
expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
|
|
257
|
-
sort: { last_message_at: -1 },
|
|
258
|
-
options: { limit: 30 },
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
const explicitSort = { updated_at: 1 as const }
|
|
262
|
-
renderWithProviders(<ChannelList {...defaultProps} sort={explicitSort} />)
|
|
263
|
-
expect(streamChannelListMock.mock.calls[1][0]).toMatchObject({
|
|
264
|
-
sort: explicitSort,
|
|
265
|
-
options: { limit: 30 },
|
|
266
|
-
})
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('provides channel list context values from props and messaging context', () => {
|
|
270
|
-
const selectedChannel = { id: 'selected-channel' } as Channel
|
|
271
|
-
const onChannelSelect = vi.fn()
|
|
272
|
-
const renderMessagePreview = vi.fn()
|
|
273
|
-
messagingContextMock.current = { debug: true }
|
|
274
|
-
|
|
275
|
-
renderWithProviders(
|
|
276
|
-
<ChannelList
|
|
277
|
-
{...defaultProps}
|
|
278
|
-
selectedChannel={selectedChannel}
|
|
279
|
-
onChannelSelect={onChannelSelect}
|
|
280
|
-
renderMessagePreview={renderMessagePreview}
|
|
281
|
-
viewerLanguage="fr"
|
|
282
|
-
/>
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
const contextValue = ChannelListProviderMock.mock.calls[0][0].value
|
|
286
|
-
expect(contextValue).toMatchObject({
|
|
287
|
-
selectedChannel,
|
|
288
|
-
onChannelSelect,
|
|
289
|
-
debug: true,
|
|
290
|
-
renderMessagePreview,
|
|
291
|
-
viewerLanguage: 'fr',
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
it('logs the channel list render when messaging debug is enabled', () => {
|
|
296
|
-
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
297
|
-
messagingContextMock.current = { debug: true }
|
|
298
|
-
|
|
299
|
-
renderWithProviders(<ChannelList {...defaultProps} />)
|
|
300
|
-
|
|
301
|
-
expect(consoleLog).toHaveBeenCalledWith(
|
|
302
|
-
'📺 [ChannelList] 🔄 RENDER START',
|
|
303
|
-
expect.objectContaining({
|
|
304
|
-
renderCount: 1,
|
|
305
|
-
selectedChannelId: undefined,
|
|
306
|
-
filters: defaultProps.filters,
|
|
307
|
-
})
|
|
308
|
-
)
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
it('passes wrapper classes and the custom empty state indicator through', () => {
|
|
312
|
-
const EmptyStateIndicator = () => <div>Empty state</div>
|
|
313
|
-
const { container } = renderWithProviders(
|
|
314
|
-
<ChannelList
|
|
315
|
-
{...defaultProps}
|
|
316
|
-
className="custom-channel-list"
|
|
317
|
-
customEmptyStateIndicator={EmptyStateIndicator}
|
|
318
|
-
/>
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
expect(container.firstElementChild).toHaveClass(
|
|
322
|
-
'messaging-channel-list',
|
|
323
|
-
'h-full',
|
|
324
|
-
'flex',
|
|
325
|
-
'flex-col',
|
|
326
|
-
'min-w-0',
|
|
327
|
-
'overflow-hidden',
|
|
328
|
-
'custom-channel-list'
|
|
329
|
-
)
|
|
330
|
-
expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
|
|
331
|
-
EmptyStateIndicator,
|
|
332
|
-
})
|
|
333
|
-
})
|
|
334
203
|
})
|
|
@@ -682,10 +682,6 @@ WithTypingIndicator.args = {
|
|
|
682
682
|
},
|
|
683
683
|
}
|
|
684
684
|
WithTypingIndicator.parameters = {
|
|
685
|
-
// The typing indicator is animation-dominant, so a snapshot mostly captures
|
|
686
|
-
// whichever frame the animation happened to pause at — a false-positive
|
|
687
|
-
// source. Skip it in Chromatic; the story stays browsable in Storybook.
|
|
688
|
-
chromatic: { disableSnapshot: true },
|
|
689
685
|
docs: {
|
|
690
686
|
description: {
|
|
691
687
|
story:
|
|
@@ -223,14 +223,10 @@ describe('ChannelView', () => {
|
|
|
223
223
|
|
|
224
224
|
it('widens the content column in the official (read-only) channel via the width var', () => {
|
|
225
225
|
const { container } = renderWithProviders(
|
|
226
|
-
<ChannelView
|
|
227
|
-
channel={createChannel({ channelType: OFFICIAL_CHANNEL_TYPE })}
|
|
228
|
-
/>
|
|
226
|
+
<ChannelView channel={createChannel({ channelType: OFFICIAL_CHANNEL_TYPE })} />
|
|
229
227
|
)
|
|
230
228
|
|
|
231
|
-
const root = container.querySelector(
|
|
232
|
-
'.messaging-channel-view'
|
|
233
|
-
) as HTMLElement
|
|
229
|
+
const root = container.querySelector('.messaging-channel-view') as HTMLElement
|
|
234
230
|
expect(root.style.getPropertyValue('--lt-msg-card-width')).toBe('368px')
|
|
235
231
|
})
|
|
236
232
|
|
|
@@ -239,9 +235,7 @@ describe('ChannelView', () => {
|
|
|
239
235
|
<ChannelView channel={createChannel({ channelType: 'messaging' })} />
|
|
240
236
|
)
|
|
241
237
|
|
|
242
|
-
const root = container.querySelector(
|
|
243
|
-
'.messaging-channel-view'
|
|
244
|
-
) as HTMLElement
|
|
238
|
+
const root = container.querySelector('.messaging-channel-view') as HTMLElement
|
|
245
239
|
expect(root.style.getPropertyValue('--lt-msg-card-width')).toBe('')
|
|
246
240
|
})
|
|
247
241
|
|
|
@@ -2,7 +2,11 @@ import '../../stream-custom-data'
|
|
|
2
2
|
|
|
3
3
|
import type { Meta, StoryFn } from '@storybook/react'
|
|
4
4
|
import React, { useEffect } from 'react'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Channel as ChannelType,
|
|
7
|
+
QueryChannelAPIResponse,
|
|
8
|
+
StreamChat,
|
|
9
|
+
} from 'stream-chat'
|
|
6
10
|
import {
|
|
7
11
|
Channel,
|
|
8
12
|
Chat,
|
|
@@ -12,20 +16,18 @@ import {
|
|
|
12
16
|
WithComponents,
|
|
13
17
|
} from 'stream-chat-react'
|
|
14
18
|
|
|
15
|
-
import {
|
|
16
|
-
Template,
|
|
17
|
-
createMockChannel,
|
|
18
|
-
type TemplateProps,
|
|
19
|
-
} from '../../stories/customMessageStoryTemplate'
|
|
19
|
+
import { minutesAgo } from '../../stories/decorators/storyTime'
|
|
20
20
|
import {
|
|
21
21
|
currentUserArgType,
|
|
22
22
|
StoryUser,
|
|
23
23
|
storyUsers,
|
|
24
24
|
} from '../../stories/decorators/storyUser'
|
|
25
25
|
import { createMockStreamChatClient } from '../../testing/createMockStreamChatClient'
|
|
26
|
+
import type { OutboundResolver } from '../../types'
|
|
26
27
|
import CustomTypingIndicator from '../CustomTypingIndicator'
|
|
27
28
|
|
|
28
29
|
import { CustomMessageActions } from './CustomMessageActions'
|
|
30
|
+
import { OutboundContext, buildOutboundsByRecordId } from './OutboundContext'
|
|
29
31
|
|
|
30
32
|
import { CustomMessage } from './index'
|
|
31
33
|
|
|
@@ -40,6 +42,180 @@ const meta: Meta = {
|
|
|
40
42
|
}
|
|
41
43
|
export default meta
|
|
42
44
|
|
|
45
|
+
const createMockChannel = async (
|
|
46
|
+
client: StreamChat,
|
|
47
|
+
messages: TemplateProps['messages']
|
|
48
|
+
) => {
|
|
49
|
+
const mockMessages = messages.map((msg, index) => ({
|
|
50
|
+
...msg,
|
|
51
|
+
type: msg.type ?? ('regular' as const),
|
|
52
|
+
created_at: minutesAgo(messages.length - index),
|
|
53
|
+
updated_at: minutesAgo(messages.length - index),
|
|
54
|
+
attachments: msg.attachments ?? [],
|
|
55
|
+
latest_reactions: [],
|
|
56
|
+
own_reactions: [],
|
|
57
|
+
reaction_counts: {},
|
|
58
|
+
reaction_scores: {},
|
|
59
|
+
reply_count: 0,
|
|
60
|
+
status: 'received',
|
|
61
|
+
cid: 'messaging:storybook-channel-1',
|
|
62
|
+
mentioned_users: msg.mentioned_users ?? [],
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
const channelData = {
|
|
66
|
+
members: [storyUsers.creator.id, storyUsers.visitor.id],
|
|
67
|
+
// Required for useUserRole: canDelete needs delete-own-message, canFlag needs flag-message
|
|
68
|
+
own_capabilities: ['delete-own-message', 'flag-message'],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const channel = client.channel(
|
|
72
|
+
'messaging',
|
|
73
|
+
'storybook-channel-1',
|
|
74
|
+
channelData
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
channel.watch = async () => {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
channel.state.messages = mockMessages as unknown as any[]
|
|
80
|
+
channel.state.members = {
|
|
81
|
+
[storyUsers.creator.id]: {
|
|
82
|
+
user: storyUsers.creator,
|
|
83
|
+
user_id: storyUsers.creator.id,
|
|
84
|
+
},
|
|
85
|
+
[storyUsers.visitor.id]: {
|
|
86
|
+
user: storyUsers.visitor,
|
|
87
|
+
user_id: storyUsers.visitor.id,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
channel: channelData,
|
|
92
|
+
members: [],
|
|
93
|
+
messages: mockMessages,
|
|
94
|
+
watchers: [],
|
|
95
|
+
pinned_messages: [],
|
|
96
|
+
duration: '0ms',
|
|
97
|
+
} as unknown as QueryChannelAPIResponse
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await channel.watch()
|
|
102
|
+
} catch {
|
|
103
|
+
// Ignore errors in mock mode
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return channel
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface TemplateProps {
|
|
110
|
+
currentUser: StoryUser
|
|
111
|
+
typingUser?: StoryUser
|
|
112
|
+
resolveOutbound?: OutboundResolver
|
|
113
|
+
messages: Array<{
|
|
114
|
+
id: string
|
|
115
|
+
text: string
|
|
116
|
+
user: StoryUser
|
|
117
|
+
mentioned_users?: StoryUser[]
|
|
118
|
+
type?: 'regular' | 'system'
|
|
119
|
+
attachments?: Array<Record<string, unknown>>
|
|
120
|
+
metadata?: {
|
|
121
|
+
custom_type?:
|
|
122
|
+
| 'MESSAGE_TIP'
|
|
123
|
+
| 'MESSAGE_PAID'
|
|
124
|
+
| 'MESSAGE_CHATBOT'
|
|
125
|
+
| 'MESSAGE_ATTACHMENT'
|
|
126
|
+
| 'MESSAGE_OUTBOUND'
|
|
127
|
+
payment_status?: 'pending' | 'paid' | 'failed' | 'refunded'
|
|
128
|
+
amount_text?: string
|
|
129
|
+
attachment_title?: string
|
|
130
|
+
attachment_mime_type?: string
|
|
131
|
+
attachment_thumbnail?: string
|
|
132
|
+
attachment_detail?: string
|
|
133
|
+
attachment_content_type?: 'text' | 'media'
|
|
134
|
+
outbound_id?: string
|
|
135
|
+
}
|
|
136
|
+
}>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const TemplateInner: React.FC<{
|
|
140
|
+
currentUser: StoryUser
|
|
141
|
+
typingUser?: StoryUser
|
|
142
|
+
messages: TemplateProps['messages']
|
|
143
|
+
resolveOutbound?: OutboundResolver
|
|
144
|
+
}> = ({ currentUser, typingUser, messages, resolveOutbound }) => {
|
|
145
|
+
const [client] = React.useState(() => createMockStreamChatClient(currentUser))
|
|
146
|
+
|
|
147
|
+
const [channel, setChannel] = React.useState<ChannelType | null>(null)
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
createMockChannel(client, messages).then(setChannel)
|
|
151
|
+
}, [client, messages])
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!channel || !typingUser) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Stream Channel UI only updates typing context from typing.start/typing.stop events.
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
client.dispatchEvent({
|
|
161
|
+
type: 'typing.start',
|
|
162
|
+
cid: channel.cid,
|
|
163
|
+
user: typingUser,
|
|
164
|
+
})
|
|
165
|
+
}, 0)
|
|
166
|
+
|
|
167
|
+
return () => clearTimeout(timer)
|
|
168
|
+
}, [channel, client, typingUser])
|
|
169
|
+
|
|
170
|
+
const outboundsByRecordId = React.useMemo(
|
|
171
|
+
() => buildOutboundsByRecordId(messages, resolveOutbound),
|
|
172
|
+
[messages, resolveOutbound]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const MessageComponent = React.useMemo(() => {
|
|
176
|
+
return function CustomMessageComponent(props: MessageUIComponentProps) {
|
|
177
|
+
return <CustomMessage {...props} onOutboundClick={() => undefined} />
|
|
178
|
+
}
|
|
179
|
+
}, [])
|
|
180
|
+
|
|
181
|
+
if (!channel) {
|
|
182
|
+
return <div className="p-4">Loading...</div>
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<OutboundContext.Provider value={outboundsByRecordId}>
|
|
187
|
+
<Chat client={client}>
|
|
188
|
+
<div className="h-screen w-full bg-white">
|
|
189
|
+
<Channel
|
|
190
|
+
channel={channel}
|
|
191
|
+
Message={MessageComponent}
|
|
192
|
+
TypingIndicator={CustomTypingIndicator}
|
|
193
|
+
>
|
|
194
|
+
<Window>
|
|
195
|
+
<MessageList />
|
|
196
|
+
</Window>
|
|
197
|
+
</Channel>
|
|
198
|
+
</div>
|
|
199
|
+
</Chat>
|
|
200
|
+
</OutboundContext.Provider>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const Template: StoryFn<TemplateProps> = ({
|
|
205
|
+
currentUser = storyUsers.creator,
|
|
206
|
+
typingUser,
|
|
207
|
+
messages,
|
|
208
|
+
resolveOutbound,
|
|
209
|
+
}) => (
|
|
210
|
+
<TemplateInner
|
|
211
|
+
key={currentUser.id}
|
|
212
|
+
currentUser={currentUser}
|
|
213
|
+
typingUser={typingUser}
|
|
214
|
+
messages={messages}
|
|
215
|
+
resolveOutbound={resolveOutbound}
|
|
216
|
+
/>
|
|
217
|
+
)
|
|
218
|
+
|
|
43
219
|
export const Default: StoryFn<TemplateProps> = Template.bind({})
|
|
44
220
|
Default.args = {
|
|
45
221
|
messages: [
|
|
@@ -166,6 +342,28 @@ OutboundStatusResolved.args = {
|
|
|
166
342
|
}),
|
|
167
343
|
}
|
|
168
344
|
|
|
345
|
+
export const WithTipTag: StoryFn<TemplateProps> = Template.bind({})
|
|
346
|
+
WithTipTag.args = {
|
|
347
|
+
messages: [
|
|
348
|
+
{
|
|
349
|
+
id: 'msg-1',
|
|
350
|
+
text: 'Love your content!',
|
|
351
|
+
user: storyUsers.visitor,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: 'msg-2',
|
|
355
|
+
text: "Here's a tip for you! Keep up the great work.",
|
|
356
|
+
user: storyUsers.visitor,
|
|
357
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.50' },
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: 'msg-3',
|
|
361
|
+
text: 'Thank you so much! 🙏',
|
|
362
|
+
user: storyUsers.creator,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
}
|
|
366
|
+
|
|
169
367
|
export const ThreeConsecutiveSentMessages: StoryFn<TemplateProps> =
|
|
170
368
|
Template.bind({})
|
|
171
369
|
ThreeConsecutiveSentMessages.args = {
|
|
@@ -193,6 +391,28 @@ ThreeConsecutiveSentMessages.args = {
|
|
|
193
391
|
],
|
|
194
392
|
}
|
|
195
393
|
|
|
394
|
+
export const TipOnly: StoryFn<TemplateProps> = Template.bind({})
|
|
395
|
+
TipOnly.args = {
|
|
396
|
+
messages: [
|
|
397
|
+
{
|
|
398
|
+
id: 'msg-1',
|
|
399
|
+
text: 'Hello!',
|
|
400
|
+
user: storyUsers.visitor,
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
id: 'msg-2',
|
|
404
|
+
text: '',
|
|
405
|
+
user: storyUsers.visitor,
|
|
406
|
+
metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$50.00' },
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: 'msg-3',
|
|
410
|
+
text: 'Wow, thank you for the tip!',
|
|
411
|
+
user: storyUsers.creator,
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
}
|
|
415
|
+
|
|
196
416
|
export const MixedTags: StoryFn<TemplateProps> = Template.bind({})
|
|
197
417
|
MixedTags.args = {
|
|
198
418
|
messages: [
|
|
@@ -248,6 +468,94 @@ MixedTags.args = {
|
|
|
248
468
|
],
|
|
249
469
|
}
|
|
250
470
|
|
|
471
|
+
export const LockedAttachment: StoryFn<TemplateProps> = Template.bind({})
|
|
472
|
+
LockedAttachment.args = {
|
|
473
|
+
messages: [
|
|
474
|
+
{
|
|
475
|
+
id: 'msg-1',
|
|
476
|
+
text: 'Can I have your workout plan?',
|
|
477
|
+
user: storyUsers.visitor,
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: 'msg-2',
|
|
481
|
+
text: 'Yes, of course!',
|
|
482
|
+
user: storyUsers.creator,
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: 'msg-3',
|
|
486
|
+
text: 'let me know if you have any questions!',
|
|
487
|
+
user: storyUsers.creator,
|
|
488
|
+
metadata: {
|
|
489
|
+
custom_type: 'MESSAGE_ATTACHMENT',
|
|
490
|
+
amount_text: 'AU$9.99',
|
|
491
|
+
attachment_title: "Alicia's Workout Plan",
|
|
492
|
+
attachment_mime_type: 'video/mp4',
|
|
493
|
+
attachment_thumbnail: '/video-thumbnail-blurred.jpg',
|
|
494
|
+
attachment_detail: '1:20',
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: 'msg-4',
|
|
499
|
+
text: 'Looks amazing, unlocking now!',
|
|
500
|
+
user: storyUsers.visitor,
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export const LockedTextAttachment: StoryFn<TemplateProps> = Template.bind({})
|
|
506
|
+
LockedTextAttachment.args = {
|
|
507
|
+
messages: [
|
|
508
|
+
{
|
|
509
|
+
id: 'msg-1',
|
|
510
|
+
text: 'Can you send me the details privately?',
|
|
511
|
+
user: storyUsers.visitor,
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: 'msg-2',
|
|
515
|
+
// Empty text: the real content is redacted server-side until purchase,
|
|
516
|
+
// so the bubble renders the blurred placeholder instead.
|
|
517
|
+
text: '',
|
|
518
|
+
user: storyUsers.creator,
|
|
519
|
+
metadata: {
|
|
520
|
+
custom_type: 'MESSAGE_ATTACHMENT',
|
|
521
|
+
amount_text: 'AU$4.99',
|
|
522
|
+
attachment_content_type: 'text',
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
id: 'msg-3',
|
|
527
|
+
text: '',
|
|
528
|
+
user: storyUsers.creator,
|
|
529
|
+
metadata: {
|
|
530
|
+
custom_type: 'MESSAGE_ATTACHMENT',
|
|
531
|
+
amount_text: 'AU$4.99',
|
|
532
|
+
payment_status: 'paid',
|
|
533
|
+
attachment_content_type: 'text',
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
id: 'msg-4',
|
|
538
|
+
// Buyer reveal: backend writes the real text back onto the message.
|
|
539
|
+
text: "Here's the address — see you Thursday at 7!",
|
|
540
|
+
user: storyUsers.creator,
|
|
541
|
+
metadata: {
|
|
542
|
+
custom_type: 'MESSAGE_ATTACHMENT',
|
|
543
|
+
amount_text: 'AU$4.99',
|
|
544
|
+
payment_status: 'paid',
|
|
545
|
+
attachment_content_type: 'text',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
}
|
|
550
|
+
LockedTextAttachment.parameters = {
|
|
551
|
+
docs: {
|
|
552
|
+
description: {
|
|
553
|
+
story:
|
|
554
|
+
'A paid text-only message renders as a regular bubble with blurred placeholder copy (msg-2: pending, msg-3: purchased-but-not-yet-revealed) until the real text is revealed (msg-4).',
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
}
|
|
558
|
+
|
|
251
559
|
export const ChatbotVariants: StoryFn<TemplateProps> = Template.bind({})
|
|
252
560
|
ChatbotVariants.args = {
|
|
253
561
|
messages: [
|
|
@@ -5,7 +5,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
5
5
|
import { renderWithProviders, screen } from '../../test/utils'
|
|
6
6
|
|
|
7
7
|
import { OutboundContext } from './OutboundContext'
|
|
8
|
-
import { isTipMessage } from './TipMessage'
|
|
9
8
|
|
|
10
9
|
import { CustomMessage } from './index'
|
|
11
10
|
|
|
@@ -59,16 +58,12 @@ vi.mock('./MessageTag', () => ({
|
|
|
59
58
|
isAttachmentMessage: () => false,
|
|
60
59
|
isChatbotMessage: () => false,
|
|
61
60
|
isMediaAttachmentMessage: () => false,
|
|
62
|
-
isPaidMessage: () => false,
|
|
63
61
|
isTextAttachmentMessage: () => false,
|
|
62
|
+
isTipOnlyMessage: () => false,
|
|
63
|
+
isTipMessage: () => false,
|
|
64
64
|
hasPaidDeliveryTag: () => false,
|
|
65
65
|
}))
|
|
66
66
|
|
|
67
|
-
vi.mock('./TipMessage', () => ({
|
|
68
|
-
TipMessage: () => null,
|
|
69
|
-
isTipMessage: vi.fn(() => false),
|
|
70
|
-
}))
|
|
71
|
-
|
|
72
67
|
vi.mock('./LockedAttachment', () => ({
|
|
73
68
|
default: () => null,
|
|
74
69
|
}))
|
|
@@ -84,11 +79,9 @@ vi.mock('../MediaMessage', () => ({
|
|
|
84
79
|
|
|
85
80
|
const mockedUseChannelStateContext = vi.mocked(useChannelStateContext)
|
|
86
81
|
const mockedUseMessageContext = vi.mocked(useMessageContext)
|
|
87
|
-
const mockedIsTipMessage = vi.mocked(isTipMessage)
|
|
88
82
|
|
|
89
83
|
describe('CustomMessage', () => {
|
|
90
84
|
beforeEach(() => {
|
|
91
|
-
mockedIsTipMessage.mockReturnValue(false)
|
|
92
85
|
mockedUseChannelStateContext.mockReturnValue({
|
|
93
86
|
messages: [{ id: 'message-1', user: { id: 'me' } }],
|
|
94
87
|
} as never)
|
|
@@ -214,51 +207,4 @@ describe('CustomMessage', () => {
|
|
|
214
207
|
screen.getByTestId('sent-message-outbound-status')
|
|
215
208
|
).toHaveTextContent('July 9th Class absentee')
|
|
216
209
|
})
|
|
217
|
-
|
|
218
|
-
it('does not render an accompanying text bubble for a whitespace-only tip message', () => {
|
|
219
|
-
mockedIsTipMessage.mockReturnValue(true)
|
|
220
|
-
mockedUseMessageContext.mockReturnValue({
|
|
221
|
-
message: {
|
|
222
|
-
id: 'message-1',
|
|
223
|
-
text: ' \n ',
|
|
224
|
-
type: 'regular',
|
|
225
|
-
status: 'received',
|
|
226
|
-
user: { id: 'me', name: 'Me' },
|
|
227
|
-
metadata: {},
|
|
228
|
-
},
|
|
229
|
-
isMyMessage: () => true,
|
|
230
|
-
threadList: false,
|
|
231
|
-
editing: false,
|
|
232
|
-
endOfGroup: true,
|
|
233
|
-
firstOfGroup: true,
|
|
234
|
-
groupedByUser: true,
|
|
235
|
-
handleAction: vi.fn(),
|
|
236
|
-
handleOpenThread: vi.fn(),
|
|
237
|
-
handleRetry: vi.fn(),
|
|
238
|
-
highlighted: false,
|
|
239
|
-
renderText: undefined,
|
|
240
|
-
isMessageAIGenerated: undefined,
|
|
241
|
-
} as never)
|
|
242
|
-
|
|
243
|
-
renderWithProviders(
|
|
244
|
-
<OutboundContext.Provider value={{}}>
|
|
245
|
-
<CustomMessage />
|
|
246
|
-
</OutboundContext.Provider>
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
expect(screen.queryByText('Message')).not.toBeInTheDocument()
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
it('renders an accompanying text bubble for a tip message with real text', () => {
|
|
253
|
-
mockedIsTipMessage.mockReturnValue(true)
|
|
254
|
-
// Default mocked message.text ('Hello') from beforeEach.
|
|
255
|
-
|
|
256
|
-
renderWithProviders(
|
|
257
|
-
<OutboundContext.Provider value={{}}>
|
|
258
|
-
<CustomMessage />
|
|
259
|
-
</OutboundContext.Provider>
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
expect(screen.getByText('Message')).toBeInTheDocument()
|
|
263
|
-
})
|
|
264
210
|
})
|