@linktr.ee/messaging-react 1.32.1 → 1.33.0-rc-1777272812
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-1CQEn-OT.js → Card-DEe10CiS.js} +2 -2
- package/dist/{Card-1CQEn-OT.js.map → Card-DEe10CiS.js.map} +1 -1
- package/dist/{Card-ClE_iExA.js → Card-Ddi8bg90.js} +2 -2
- package/dist/{Card-ClE_iExA.js.map → Card-Ddi8bg90.js.map} +1 -1
- package/dist/index-BePLvyvi.js +2868 -0
- package/dist/index-BePLvyvi.js.map +1 -0
- package/dist/index.d.ts +19 -1
- package/dist/index.js +20 -2477
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChannelView.stories.tsx +38 -0
- package/src/components/ChannelView.test.tsx +25 -6
- package/src/components/ChannelView.tsx +2 -0
- package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +180 -0
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +63 -1
- package/src/components/CustomMessageInput/index.tsx +24 -5
- package/src/components/MediaMessage/MediaMessage.stories.tsx +170 -0
- package/src/components/MediaMessage/MediaMessage.test.tsx +261 -0
- package/src/components/MediaMessage/index.tsx +165 -0
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/index.ts +2 -0
- package/src/types.ts +13 -0
- package/dist/MediaPlayer-B9Ws2NeE.js +0 -292
- package/dist/MediaPlayer-B9Ws2NeE.js.map +0 -1
|
@@ -7,17 +7,17 @@ import { renderWithProviders, screen } from '../test/utils'
|
|
|
7
7
|
import { ChannelView } from './ChannelView'
|
|
8
8
|
|
|
9
9
|
let activeChannel: unknown
|
|
10
|
+
let activeChannelProps: Record<string, unknown> = {}
|
|
10
11
|
|
|
11
12
|
vi.mock('stream-chat-react', () => ({
|
|
12
|
-
Channel: ({
|
|
13
|
-
channel,
|
|
14
|
-
children,
|
|
15
|
-
}: {
|
|
13
|
+
Channel: (props: {
|
|
16
14
|
channel: unknown
|
|
17
15
|
children: React.ReactNode
|
|
16
|
+
[key: string]: unknown
|
|
18
17
|
}) => {
|
|
19
|
-
activeChannel = channel
|
|
20
|
-
|
|
18
|
+
activeChannel = props.channel
|
|
19
|
+
activeChannelProps = props
|
|
20
|
+
return <div data-testid="channel">{props.children}</div>
|
|
21
21
|
},
|
|
22
22
|
Window: ({ children }: { children: React.ReactNode }) => (
|
|
23
23
|
<div data-testid="window">{children}</div>
|
|
@@ -96,6 +96,7 @@ const createChannel = () =>
|
|
|
96
96
|
describe('ChannelView', () => {
|
|
97
97
|
beforeEach(() => {
|
|
98
98
|
activeChannel = undefined
|
|
99
|
+
activeChannelProps = {}
|
|
99
100
|
avatarRenderCalls.length = 0
|
|
100
101
|
mockIsStarred = false
|
|
101
102
|
})
|
|
@@ -193,4 +194,22 @@ describe('ChannelView', () => {
|
|
|
193
194
|
)
|
|
194
195
|
expect(renderMessageInputActions).toHaveBeenCalledWith(channel)
|
|
195
196
|
})
|
|
197
|
+
|
|
198
|
+
it('passes sendButton to Channel as SendButton when provided', () => {
|
|
199
|
+
const CustomSendButton = () => (
|
|
200
|
+
<button type="button" aria-label="Custom Send" />
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
renderWithProviders(
|
|
204
|
+
<ChannelView channel={createChannel()} sendButton={CustomSendButton} />
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(activeChannelProps.SendButton).toBe(CustomSendButton)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('does not pass SendButton to Channel when sendButton is not provided', () => {
|
|
211
|
+
renderWithProviders(<ChannelView channel={createChannel()} />)
|
|
212
|
+
|
|
213
|
+
expect(activeChannelProps.SendButton).toBeUndefined()
|
|
214
|
+
})
|
|
196
215
|
})
|
|
@@ -376,6 +376,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
376
376
|
customProfileContent,
|
|
377
377
|
customChannelActions,
|
|
378
378
|
renderMessage,
|
|
379
|
+
sendButton,
|
|
379
380
|
}) => {
|
|
380
381
|
// Custom send message handler that:
|
|
381
382
|
// 1. Applies messageMetadata if provided
|
|
@@ -435,6 +436,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
435
436
|
LoadingIndicator={LoadingState}
|
|
436
437
|
DateSeparator={CustomDateSeparator}
|
|
437
438
|
doSendMessageRequest={doSendMessageRequest}
|
|
439
|
+
{...(sendButton ? { SendButton: sendButton } : {})}
|
|
438
440
|
>
|
|
439
441
|
<ChannelViewInner
|
|
440
442
|
onBack={onBack}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React, { useEffect } from 'react'
|
|
3
|
+
import { QueryChannelAPIResponse, StreamChat } from 'stream-chat'
|
|
4
|
+
import { Channel, Chat } from 'stream-chat-react'
|
|
5
|
+
|
|
6
|
+
import { CustomMessageInput } from '.'
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Setup helpers (mirrors ChannelView.stories.tsx)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const mockUser = {
|
|
13
|
+
id: 'storybook-user',
|
|
14
|
+
name: 'Storybook User',
|
|
15
|
+
image: 'https://i.pravatar.cc/150?img=1',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const mockParticipant = {
|
|
19
|
+
id: 'participant-1',
|
|
20
|
+
name: 'Jane Creator',
|
|
21
|
+
image: 'https://i.pravatar.cc/150?img=5',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const createMockChannel = async (
|
|
25
|
+
client: StreamChat,
|
|
26
|
+
opts: { frozen?: boolean } = {}
|
|
27
|
+
) => {
|
|
28
|
+
const channelData = { members: [mockUser.id, mockParticipant.id], frozen: opts.frozen ?? false }
|
|
29
|
+
const ch = client.channel('messaging', 'storybook-cmi-channel', channelData)
|
|
30
|
+
|
|
31
|
+
ch.watch = async () => {
|
|
32
|
+
ch.state.members = {
|
|
33
|
+
[mockUser.id]: { user: mockUser, user_id: mockUser.id },
|
|
34
|
+
[mockParticipant.id]: { user: mockParticipant, user_id: mockParticipant.id },
|
|
35
|
+
}
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
;(ch as any)._data = channelData
|
|
38
|
+
return { channel: channelData, members: [], messages: [], watchers: [], pinned_messages: [], duration: '0ms' } as unknown as QueryChannelAPIResponse
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try { await ch.watch() } catch (_) { /* mock */ }
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
;(ch as any)._data = channelData
|
|
44
|
+
return ch
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Story wrapper — mounts the input inside a real Stream Channel context
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
type WrapperProps = {
|
|
52
|
+
frozen?: boolean
|
|
53
|
+
sendButton?: React.ComponentType<{
|
|
54
|
+
sendMessage: (...args: unknown[]) => unknown
|
|
55
|
+
disabled?: boolean
|
|
56
|
+
[key: string]: unknown
|
|
57
|
+
}>
|
|
58
|
+
renderActions?: () => React.ReactNode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const Wrapper: React.FC<WrapperProps> = ({ frozen, sendButton, renderActions }) => {
|
|
62
|
+
const [client] = React.useState(() => {
|
|
63
|
+
const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
|
|
64
|
+
c.userID = mockUser.id
|
|
65
|
+
c.user = mockUser
|
|
66
|
+
return c
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const [channel, setChannel] = React.useState<ReturnType<StreamChat['channel']> | null>(null)
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
createMockChannel(client, { frozen }).then(setChannel)
|
|
73
|
+
}, [client, frozen])
|
|
74
|
+
|
|
75
|
+
if (!channel) return <div className="p-4 text-sm text-gray-400">Loading…</div>
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Chat client={client}>
|
|
79
|
+
<Channel
|
|
80
|
+
channel={channel}
|
|
81
|
+
{...(sendButton ? { SendButton: sendButton } : {})}
|
|
82
|
+
>
|
|
83
|
+
<div className="bg-white" style={{ minWidth: 360 }}>
|
|
84
|
+
<CustomMessageInput renderActions={renderActions} />
|
|
85
|
+
</div>
|
|
86
|
+
</Channel>
|
|
87
|
+
</Chat>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Meta
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
const meta: Meta<WrapperProps> = {
|
|
96
|
+
title: 'CustomMessageInput',
|
|
97
|
+
component: CustomMessageInput,
|
|
98
|
+
parameters: {
|
|
99
|
+
layout: 'centered',
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
export default meta
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Stories
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export const Default: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
109
|
+
Default.args = {}
|
|
110
|
+
Default.parameters = {
|
|
111
|
+
docs: {
|
|
112
|
+
description: {
|
|
113
|
+
story:
|
|
114
|
+
'Default message input. The send button (arrow) is disabled until the user types text — `useMessageComposerHasSendableData` controls this.',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const Frozen: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
120
|
+
Frozen.args = { frozen: true }
|
|
121
|
+
Frozen.parameters = {
|
|
122
|
+
docs: {
|
|
123
|
+
description: {
|
|
124
|
+
story:
|
|
125
|
+
'Frozen channel state: the entire input area fades out (`aria-disabled`, `inert`), the textarea becomes read-only, and the send button is disabled.',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const WithCustomActionButton: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
131
|
+
WithCustomActionButton.args = {
|
|
132
|
+
renderActions: () => (
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
aria-label="Attach media"
|
|
136
|
+
className="flex items-center justify-center size-10 rounded-full bg-[#F1F0EE] hover:bg-[#E5E4E1] shrink-0"
|
|
137
|
+
onClick={() => console.log('media picker')}
|
|
138
|
+
>
|
|
139
|
+
📷
|
|
140
|
+
</button>
|
|
141
|
+
),
|
|
142
|
+
}
|
|
143
|
+
WithCustomActionButton.parameters = {
|
|
144
|
+
docs: {
|
|
145
|
+
description: {
|
|
146
|
+
story:
|
|
147
|
+
'Shows how `renderActions` injects extra controls (e.g. a media picker button) to the left of the message bubble.',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A custom send button that turns purple to signal a media attachment is staged. */
|
|
153
|
+
const PurpleMediaSendButton: React.FC<{
|
|
154
|
+
sendMessage: (...args: unknown[]) => unknown
|
|
155
|
+
disabled?: boolean
|
|
156
|
+
[key: string]: unknown
|
|
157
|
+
}> = ({ sendMessage, disabled, ...rest }) => (
|
|
158
|
+
<button
|
|
159
|
+
{...rest}
|
|
160
|
+
type="button"
|
|
161
|
+
aria-label="Send"
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
onClick={() => sendMessage()}
|
|
164
|
+
className="str-chat__send-button mt-auto flex items-center justify-center shrink-0 rounded-full size-8 bg-[#6D28D9] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white"
|
|
165
|
+
style={{ fontSize: 15 }}
|
|
166
|
+
>
|
|
167
|
+
🚀
|
|
168
|
+
</button>
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
export const WithCustomSendButton: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
172
|
+
WithCustomSendButton.args = { sendButton: PurpleMediaSendButton }
|
|
173
|
+
WithCustomSendButton.parameters = {
|
|
174
|
+
docs: {
|
|
175
|
+
description: {
|
|
176
|
+
story:
|
|
177
|
+
'Passes a custom `SendButton` via `Channel`\'s `SendButton` prop (which is how `ChannelView`\'s `sendButton` prop surfaces it). `CustomMessageInput` reads the button from `useComponentContext` and falls back to the default when none is provided.',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { renderWithProviders, screen } from '../../test/utils'
|
|
5
5
|
|
|
@@ -7,6 +7,21 @@ import { CustomMessageInput } from '.'
|
|
|
7
7
|
|
|
8
8
|
let mockChannelData: Record<string, unknown> = {}
|
|
9
9
|
|
|
10
|
+
let mockContextSendButton:
|
|
11
|
+
| React.ComponentType<{
|
|
12
|
+
sendMessage: (...args: unknown[]) => unknown
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
}>
|
|
15
|
+
| undefined = ({ sendMessage, disabled }) => (
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
data-testid="send-button"
|
|
19
|
+
aria-label="Send"
|
|
20
|
+
disabled={disabled}
|
|
21
|
+
onClick={() => sendMessage()}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
|
|
10
25
|
vi.mock('stream-chat-react', () => ({
|
|
11
26
|
MessageInput: ({
|
|
12
27
|
Input,
|
|
@@ -35,6 +50,9 @@ vi.mock('stream-chat-react', () => ({
|
|
|
35
50
|
handleSubmit: vi.fn(),
|
|
36
51
|
}),
|
|
37
52
|
useMessageComposerHasSendableData: () => false,
|
|
53
|
+
useComponentContext: () => ({
|
|
54
|
+
SendButton: mockContextSendButton,
|
|
55
|
+
}),
|
|
38
56
|
}))
|
|
39
57
|
|
|
40
58
|
vi.mock('../CustomLinkPreviewList', () => ({
|
|
@@ -42,6 +60,18 @@ vi.mock('../CustomLinkPreviewList', () => ({
|
|
|
42
60
|
}))
|
|
43
61
|
|
|
44
62
|
describe('CustomMessageInput', () => {
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
mockContextSendButton = ({ sendMessage, disabled }) => (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
data-testid="send-button"
|
|
68
|
+
aria-label="Send"
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
onClick={() => sendMessage()}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
45
75
|
it('renders the interactive message input when channel is not frozen', () => {
|
|
46
76
|
mockChannelData = {}
|
|
47
77
|
|
|
@@ -124,4 +154,36 @@ describe('CustomMessageInput', () => {
|
|
|
124
154
|
expect(messageInput).not.toHaveAttribute('aria-disabled')
|
|
125
155
|
expect(screen.getByTestId('custom-action')).toBeInTheDocument()
|
|
126
156
|
})
|
|
157
|
+
|
|
158
|
+
it('renders the custom SendButton from component context', () => {
|
|
159
|
+
mockChannelData = {}
|
|
160
|
+
mockContextSendButton = () => (
|
|
161
|
+
<button type="button" data-testid="custom-context-send-button" aria-label="Custom Send" />
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
renderWithProviders(<CustomMessageInput />)
|
|
165
|
+
|
|
166
|
+
expect(screen.getByTestId('custom-context-send-button')).toBeInTheDocument()
|
|
167
|
+
expect(screen.queryByTestId('default-send-button')).not.toBeInTheDocument()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('falls back to the arrow send button when component context provides none', () => {
|
|
171
|
+
mockChannelData = {}
|
|
172
|
+
mockContextSendButton = undefined
|
|
173
|
+
|
|
174
|
+
renderWithProviders(<CustomMessageInput />)
|
|
175
|
+
|
|
176
|
+
expect(screen.getByTestId('send-button')).toBeInTheDocument()
|
|
177
|
+
expect(screen.queryByTestId('custom-context-send-button')).not.toBeInTheDocument()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('preserves disabled state on the send button when there is no sendable data', () => {
|
|
181
|
+
mockChannelData = {}
|
|
182
|
+
|
|
183
|
+
renderWithProviders(<CustomMessageInput />)
|
|
184
|
+
|
|
185
|
+
const sendButton = screen.getByRole('button', { name: /send/i })
|
|
186
|
+
// useMessageComposerHasSendableData is mocked to return false
|
|
187
|
+
expect(sendButton).toBeDisabled()
|
|
188
|
+
})
|
|
127
189
|
})
|
|
@@ -7,16 +7,37 @@ import {
|
|
|
7
7
|
SimpleAttachmentSelector,
|
|
8
8
|
TextareaComposer,
|
|
9
9
|
useChannelStateContext,
|
|
10
|
+
useComponentContext,
|
|
10
11
|
useMessageComposerHasSendableData,
|
|
11
12
|
useMessageInputContext,
|
|
12
13
|
} from 'stream-chat-react'
|
|
13
14
|
|
|
14
15
|
import { CustomLinkPreviewList } from '../CustomLinkPreviewList'
|
|
15
16
|
|
|
17
|
+
const DefaultSendButton: React.FC<{
|
|
18
|
+
sendMessage: () => void
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
[key: string]: unknown
|
|
21
|
+
}> = ({ sendMessage, disabled, ...rest }) => (
|
|
22
|
+
<button
|
|
23
|
+
{...rest}
|
|
24
|
+
type="button"
|
|
25
|
+
aria-label="Send"
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
onClick={sendMessage}
|
|
28
|
+
>
|
|
29
|
+
<ArrowUpIcon weight="bold" className="size-4" />
|
|
30
|
+
</button>
|
|
31
|
+
)
|
|
32
|
+
|
|
16
33
|
const CustomMessageInputInner: React.FC = () => {
|
|
17
34
|
const { channel } = useChannelStateContext()
|
|
18
35
|
const isFrozen = channel?.data?.frozen === true
|
|
19
36
|
const { handleSubmit } = useMessageInputContext()
|
|
37
|
+
const { SendButton: SendButtonFromContext } = useComponentContext(
|
|
38
|
+
'CustomMessageInput',
|
|
39
|
+
)
|
|
40
|
+
const SendButton = SendButtonFromContext ?? DefaultSendButton
|
|
20
41
|
const hasSendableData = useMessageComposerHasSendableData()
|
|
21
42
|
const isSendDisabled = isFrozen || !hasSendableData
|
|
22
43
|
|
|
@@ -44,16 +65,14 @@ const CustomMessageInputInner: React.FC = () => {
|
|
|
44
65
|
tabIndex={isFrozen ? -1 : undefined}
|
|
45
66
|
/>
|
|
46
67
|
</div>
|
|
47
|
-
<
|
|
68
|
+
<SendButton
|
|
69
|
+
sendMessage={handleSubmit}
|
|
48
70
|
aria-label="Send"
|
|
49
71
|
className="str-chat__send-button mt-auto flex justify-center items-center flex-shrink-0 rounded-full size-8 bg-[#121110] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white focus-ring"
|
|
50
72
|
data-testid="send-button"
|
|
51
73
|
disabled={isSendDisabled}
|
|
52
|
-
onClick={handleSubmit}
|
|
53
74
|
type="button"
|
|
54
|
-
|
|
55
|
-
<ArrowUpIcon className="size-4" />
|
|
56
|
-
</button>
|
|
75
|
+
/>
|
|
57
76
|
</div>
|
|
58
77
|
</div>
|
|
59
78
|
</>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import type { LocalMessage } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
import { MediaMessage } from '.'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof MediaMessage> = {
|
|
8
|
+
title: 'Components/MediaMessage',
|
|
9
|
+
component: MediaMessage,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'centered',
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
export default meta
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Shared helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const SENDER = { id: 'user-other', name: 'Jane Creator', image: 'https://i.pravatar.cc/40?img=5' }
|
|
21
|
+
const ME = { id: 'user-me', name: 'Me' }
|
|
22
|
+
|
|
23
|
+
const base = (overrides: Partial<LocalMessage> = {}): LocalMessage => ({
|
|
24
|
+
id: 'msg-1',
|
|
25
|
+
text: '',
|
|
26
|
+
type: 'regular',
|
|
27
|
+
created_at: new Date(),
|
|
28
|
+
updated_at: new Date(),
|
|
29
|
+
user: SENDER,
|
|
30
|
+
attachments: [],
|
|
31
|
+
...overrides,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const VARIANTS = [
|
|
35
|
+
{
|
|
36
|
+
label: 'Video',
|
|
37
|
+
attachment: {
|
|
38
|
+
type: 'video',
|
|
39
|
+
asset_url: 'https://www.w3schools.com/html/mov_bbb.mp4',
|
|
40
|
+
mime_type: 'video/mp4',
|
|
41
|
+
title: "Alicia's Workout Plan",
|
|
42
|
+
file_size: 788_456,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: 'Audio',
|
|
47
|
+
attachment: {
|
|
48
|
+
type: 'audio',
|
|
49
|
+
asset_url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
|
50
|
+
mime_type: 'audio/mpeg',
|
|
51
|
+
title: 'Morning Meditation',
|
|
52
|
+
file_size: 6_900_000,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: 'Image',
|
|
57
|
+
attachment: {
|
|
58
|
+
type: 'image',
|
|
59
|
+
image_url: 'https://picsum.photos/seed/storybook/560/315',
|
|
60
|
+
mime_type: 'image/jpeg',
|
|
61
|
+
title: 'Picture of my cat',
|
|
62
|
+
file_size: 3_355_443,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: 'Document',
|
|
67
|
+
attachment: {
|
|
68
|
+
type: 'file',
|
|
69
|
+
asset_url: 'https://www.w3.org/WAI/WCAG21/wcag21.pdf',
|
|
70
|
+
mime_type: 'application/pdf',
|
|
71
|
+
title: 'Strength Training Guide',
|
|
72
|
+
file_size: 1_240_000,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: 'Unknown',
|
|
77
|
+
attachment: {
|
|
78
|
+
type: 'file',
|
|
79
|
+
asset_url: 'https://www.w3.org/WAI/WCAG21/wcag21.pdf',
|
|
80
|
+
mime_type: 'application/octet-stream',
|
|
81
|
+
title: 'Unknown Attachment',
|
|
82
|
+
file_size: 456_000,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
] as const
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Layout primitives
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
const GridTable: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
92
|
+
<div className="p-12 bg-[#F9F7F4]">
|
|
93
|
+
<table className="border-separate border-spacing-4">{children}</table>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const GridHead = () => (
|
|
98
|
+
<thead>
|
|
99
|
+
<tr>
|
|
100
|
+
<th className="text-left text-xs font-medium text-black/40 pb-2 w-16" />
|
|
101
|
+
{VARIANTS.map(({ label }) => (
|
|
102
|
+
<th key={label} className="text-left text-xs font-medium text-black/40 pb-2">
|
|
103
|
+
{label}
|
|
104
|
+
</th>
|
|
105
|
+
))}
|
|
106
|
+
</tr>
|
|
107
|
+
</thead>
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const RowLabel: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
111
|
+
<td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
|
|
112
|
+
{children}
|
|
113
|
+
</td>
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Stories
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export const Visitor: StoryFn = () => (
|
|
121
|
+
<GridTable>
|
|
122
|
+
<GridHead />
|
|
123
|
+
<tbody>
|
|
124
|
+
<tr>
|
|
125
|
+
<RowLabel>Sent</RowLabel>
|
|
126
|
+
{VARIANTS.map(({ label, attachment }) => (
|
|
127
|
+
<td key={label} className="align-top">
|
|
128
|
+
<MediaMessage
|
|
129
|
+
isMyMessage={false}
|
|
130
|
+
message={base({ user: SENDER, attachments: [attachment as LocalMessage['attachments'][number]] })}
|
|
131
|
+
/>
|
|
132
|
+
</td>
|
|
133
|
+
))}
|
|
134
|
+
</tr>
|
|
135
|
+
</tbody>
|
|
136
|
+
</GridTable>
|
|
137
|
+
)
|
|
138
|
+
Visitor.parameters = {
|
|
139
|
+
docs: {
|
|
140
|
+
description: {
|
|
141
|
+
story: 'Visitor perspective — messages from the creator, left-aligned with avatar.',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const Creator: StoryFn = () => (
|
|
147
|
+
<GridTable>
|
|
148
|
+
<GridHead />
|
|
149
|
+
<tbody>
|
|
150
|
+
<tr>
|
|
151
|
+
<RowLabel>Sent</RowLabel>
|
|
152
|
+
{VARIANTS.map(({ label, attachment }) => (
|
|
153
|
+
<td key={label} className="align-top">
|
|
154
|
+
<MediaMessage
|
|
155
|
+
isMyMessage={true}
|
|
156
|
+
message={base({ user: ME, attachments: [attachment as LocalMessage['attachments'][number]] })}
|
|
157
|
+
/>
|
|
158
|
+
</td>
|
|
159
|
+
))}
|
|
160
|
+
</tr>
|
|
161
|
+
</tbody>
|
|
162
|
+
</GridTable>
|
|
163
|
+
)
|
|
164
|
+
Creator.parameters = {
|
|
165
|
+
docs: {
|
|
166
|
+
description: {
|
|
167
|
+
story: 'Creator perspective — own messages, right-aligned, no avatar.',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
}
|