@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.
Files changed (96) hide show
  1. package/dist/{Card-BYJyA83X.js → Card-D7UPq5-g.js} +2 -2
  2. package/dist/{Card-BYJyA83X.js.map → Card-D7UPq5-g.js.map} +1 -1
  3. package/dist/{Card-BplJtZao.cjs → Card-DcBZqMBt.cjs} +2 -2
  4. package/dist/{Card-BplJtZao.cjs.map → Card-DcBZqMBt.cjs.map} +1 -1
  5. package/dist/assets/index.css +1 -1
  6. package/dist/{index-BUC9vqhJ.js → index-CSkr_CO5.js} +2089 -1952
  7. package/dist/index-CSkr_CO5.js.map +1 -0
  8. package/dist/index-CwkY7QwT.cjs +3 -0
  9. package/dist/index-CwkY7QwT.cjs.map +1 -0
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.d.ts +59 -78
  12. package/dist/index.js +26 -30
  13. package/dist/testing.cjs +1 -1
  14. package/dist/testing.cjs.map +1 -1
  15. package/dist/testing.js +2 -2
  16. package/dist/testing.js.map +1 -1
  17. package/package.json +13 -11
  18. package/src/components/AttachmentCard/utils/icons.ts +7 -6
  19. package/src/components/Avatar/Avatar.test.tsx +0 -126
  20. package/src/components/Avatar/getAvatarEmoji.test.ts +0 -39
  21. package/src/components/ChannelList/CustomChannelPreview.test.tsx +3 -410
  22. package/src/components/ChannelList/index.test.tsx +1 -132
  23. package/src/components/ChannelView.stories.tsx +0 -4
  24. package/src/components/ChannelView.test.tsx +3 -9
  25. package/src/components/CustomMessage/CustomMessage.stories.tsx +314 -6
  26. package/src/components/CustomMessage/CustomMessage.test.tsx +2 -56
  27. package/src/components/CustomMessage/MessageTag.stories.tsx +53 -17
  28. package/src/components/CustomMessage/MessageTag.test.tsx +40 -7
  29. package/src/components/CustomMessage/MessageTag.tsx +39 -7
  30. package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +7 -8
  31. package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +1 -2
  32. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
  33. package/src/components/CustomMessage/StreamAttachmentMessage.tsx +12 -5
  34. package/src/components/CustomMessage/index.tsx +19 -22
  35. package/src/components/CustomMessageInput/index.tsx +1 -1
  36. package/src/components/CustomSystemMessage/index.tsx +2 -3
  37. package/src/components/LinkAttachment/LinkAttachment.test.tsx +0 -327
  38. package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +165 -58
  39. package/src/components/MessageAttachment/Audio/WaveformAudioRow.test.tsx +153 -0
  40. package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +450 -0
  41. package/src/components/MessageAttachment/Audio/audioRowRequester.test.ts +24 -0
  42. package/src/components/MessageAttachment/Audio/audioRowRequester.ts +16 -0
  43. package/src/components/MessageAttachment/Audio/index.tsx +83 -77
  44. package/src/components/MessageAttachment/MessageAttachment.test.tsx +176 -74
  45. package/src/components/MessageAttachment/_shared/fileMeta.test.ts +0 -44
  46. package/src/components/MessageAttachment/_shared/useIsClient.ts +23 -0
  47. package/src/components/MessageAttachment/index.tsx +25 -9
  48. package/src/components/MessageAttachment/types.ts +19 -9
  49. package/src/components/MessagingShell/MessagingShell.test.tsx +0 -20
  50. package/src/components/MessagingShell/index.tsx +0 -2
  51. package/src/components/RichCard/RichCard.test.tsx +1 -3
  52. package/src/index.ts +0 -4
  53. package/src/providers/MessagingProvider.test.tsx +2 -130
  54. package/src/providers/MessagingProvider.tsx +15 -26
  55. package/src/styles.css +210 -57
  56. package/src/testing/createMockMessagingClient.ts +2 -2
  57. package/src/types.ts +0 -9
  58. package/dist/index-BUC9vqhJ.js.map +0 -1
  59. package/dist/index-BkqyKPJP.cjs +0 -3
  60. package/dist/index-BkqyKPJP.cjs.map +0 -1
  61. package/src/components/AttachmentCard/MediaPlayer.test.tsx +0 -346
  62. package/src/components/AttachmentCard/Thumbnail.test.tsx +0 -215
  63. package/src/components/AttachmentCard/index.test.tsx +0 -160
  64. package/src/components/ChannelList/ChannelListContext.test.tsx +0 -87
  65. package/src/components/CustomMessage/Locked.stories.tsx +0 -108
  66. package/src/components/CustomMessage/PaidMessage.stories.tsx +0 -42
  67. package/src/components/CustomMessage/Tip.stories.tsx +0 -48
  68. package/src/components/CustomMessage/TipMessage/TipMessage.stories.tsx +0 -51
  69. package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +0 -65
  70. package/src/components/CustomMessage/TipMessage/index.tsx +0 -27
  71. package/src/components/FaqList/FaqList.test.tsx +0 -204
  72. package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +0 -76
  73. package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +0 -248
  74. package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +0 -123
  75. package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +0 -193
  76. package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +0 -242
  77. package/src/components/LinkAttachment/components/_shared/hex.test.ts +0 -37
  78. package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +0 -72
  79. package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +0 -162
  80. package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +0 -182
  81. package/src/components/MessageAttachment/_shared/Bubble.test.tsx +0 -111
  82. package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +0 -36
  83. package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +0 -114
  84. package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +0 -42
  85. package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +0 -168
  86. package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +0 -52
  87. package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +0 -69
  88. package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +0 -45
  89. package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +0 -55
  90. package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +0 -246
  91. package/src/components/MessageAttachment/_shared/triggerDownload.test.ts +0 -113
  92. package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +0 -68
  93. package/src/components/MessageAttachment/_shared/viewerChromeClasses.test.ts +0 -16
  94. package/src/logging/index.test.tsx +0 -136
  95. package/src/logging/index.tsx +0 -48
  96. package/src/stories/customMessageStoryTemplate.tsx +0 -202
@@ -27,130 +27,4 @@ describe('Avatar', () => {
27
27
  it('keeps fixed emoji sizes for larger avatars', () => {
28
28
  expect(emojiFontSize(40)).toHaveStyle({ fontSize: '14px' })
29
29
  })
30
-
31
- it.each([
32
- [32, '14px'],
33
- [55, '14px'],
34
- [56, '18px'],
35
- [119, '18px'],
36
- [120, '36px'],
37
- ])(
38
- 'uses the exact fixed font size at the %spx boundary',
39
- (size, fontSize) => {
40
- expect(emojiFontSize(size)).toHaveStyle({ fontSize })
41
- }
42
- )
43
-
44
- it('does not show a star by default but shows one when starred', () => {
45
- const defaultAvatar = renderWithProviders(
46
- <Avatar id="user-1" name="Alice" />
47
- )
48
- expect(defaultAvatar.container.querySelector('svg')).not.toBeInTheDocument()
49
-
50
- const starredAvatar = renderWithProviders(
51
- <Avatar id="user-1" name="Alice" starred />
52
- )
53
- expect(starredAvatar.container.querySelector('svg')).toBeInTheDocument()
54
- })
55
-
56
- it('uses a squircle by default and a circle when explicitly requested', () => {
57
- const squircleAvatar = renderWithProviders(
58
- <Avatar id="user-1" name="Alice" />
59
- )
60
- const squircleElements = squircleAvatar.container.querySelectorAll(
61
- '[style*="border-radius"]'
62
- )
63
- expect(squircleElements).toHaveLength(3)
64
- squircleElements.forEach((element) =>
65
- expect(element).toHaveStyle({ borderRadius: '1rem' })
66
- )
67
-
68
- const circleAvatar = renderWithProviders(
69
- <Avatar id="user-1" name="Alice" shape="circle" />
70
- )
71
- const circleElements = circleAvatar.container.querySelectorAll(
72
- '[style*="border-radius"]'
73
- )
74
- expect(circleElements).toHaveLength(3)
75
- circleElements.forEach((element) =>
76
- expect(element).toHaveStyle({ borderRadius: '50%' })
77
- )
78
- })
79
-
80
- it('only applies the AI ring when dmAgentEnabled is true', () => {
81
- const defaultAvatar = renderWithProviders(
82
- <Avatar id="user-1" name="Alice" size={40} />
83
- )
84
- expect(
85
- defaultAvatar.container.querySelector('[data-testid="avatar-ring"]')
86
- ).not.toHaveStyle({
87
- borderWidth: '2px',
88
- borderStyle: 'solid',
89
- })
90
-
91
- const enabledAvatar = renderWithProviders(
92
- <Avatar id="user-1" name="Alice" size={40} dmAgentEnabled />
93
- )
94
- expect(
95
- enabledAvatar.container.querySelector('[data-testid="avatar-ring"]')
96
- ).toHaveStyle({
97
- borderWidth: '2px',
98
- borderStyle: 'solid',
99
- borderColor: 'var(--AI-Gradient, #7F22FE)',
100
- boxShadow: 'inset 0 1px 2px 0 rgba(255, 255, 255, 0.5)',
101
- })
102
-
103
- const smallEnabledAvatar = renderWithProviders(
104
- <Avatar id="user-1" name="Alice" size={20} dmAgentEnabled />
105
- )
106
- expect(
107
- smallEnabledAvatar.container.querySelector('[data-testid="avatar-ring"]')
108
- ).toHaveStyle({ borderWidth: '1px' })
109
- })
110
-
111
- it('renders an image instead of the emoji fallback when image is provided', () => {
112
- const fallbackAvatar = renderWithProviders(<Avatar id="" name="Alice" />)
113
- expect(
114
- fallbackAvatar.container.querySelector('img')
115
- ).not.toBeInTheDocument()
116
- expect(fallbackAvatar.container).toHaveTextContent('🍎')
117
- expect(
118
- fallbackAvatar.container.querySelector('.avatar-fallback')
119
- ).toHaveClass(
120
- 'avatar-fallback',
121
- 'flex',
122
- 'h-full',
123
- 'w-full',
124
- 'items-center',
125
- 'justify-center',
126
- 'font-semibold',
127
- 'select-none',
128
- 'transition-colors'
129
- )
130
-
131
- const imageAvatar = renderWithProviders(
132
- <Avatar id="" name="Alice" image="https://example.com/avatar.png" />
133
- )
134
- expect(imageAvatar.container.querySelector('img')).toHaveAttribute(
135
- 'src',
136
- 'https://example.com/avatar.png'
137
- )
138
- expect(imageAvatar.container).not.toHaveTextContent('🍎')
139
- })
140
-
141
- it('applies the fallback background color and forwards custom classes', () => {
142
- const { container } = renderWithProviders(
143
- <Avatar id="" name="Alice" className="custom-avatar" />
144
- )
145
- const root = container.firstElementChild
146
- const ring = container.querySelector('[data-testid="avatar-ring"]')
147
-
148
- expect(root).toHaveClass('relative', 'flex-shrink-0', 'custom-avatar')
149
- expect(ring).toHaveClass('h-full', 'w-full', 'bg-transparent')
150
- expect(root).toHaveStyle({
151
- backgroundColor: '#FFD1DD',
152
- width: '40px',
153
- height: '40px',
154
- })
155
- })
156
30
  })
@@ -29,43 +29,4 @@ describe('getAvatarBgColor', () => {
29
29
  expect(color).toBe(emojiBgColors[emoji])
30
30
  expect(getAvatarBgColor(id)).toBe(color)
31
31
  })
32
-
33
- it.each([
34
- ['', '🍎', '#FFD1DD'],
35
- ['id-2', '🍌', '#FFF1C9'],
36
- ['id-3', '🍇', '#E6D9FF'],
37
- ['id-4', '🍊', '#D6F4FF'],
38
- ['id-5', '🍓', '#FFE0AE'],
39
- ['id-6', '🥥', '#F7E7D8'],
40
- ['id-7', '🍒', '#DFF4D8'],
41
- ['id-8', '🥭', '#FFD8C2'],
42
- ['id-9', '🍉', '#D6EBFF'],
43
- ['id-10', '🍋', '#CAEEB5'],
44
- ['id-11', '🥝', '#FFE7C7'],
45
- ['id-12', '🫒', '#F6E3B4'],
46
- ['id-13', '🍈', '#D9F7E1'],
47
- ])('returns the exact fruit pair for %j', (id, emoji, bgColor) => {
48
- expect(getAvatarEmoji(id)).toBe(emoji)
49
- expect(getAvatarBgColor(id)).toBe(bgColor)
50
- })
51
-
52
- it('uses the absolute hash value for ids with a negative raw hash', () => {
53
- const id = 'id-100'
54
-
55
- expect(getAvatarEmoji(id)).toBe('🥭')
56
- expect(getAvatarBgColor(id)).toBe('#FFD8C2')
57
- })
58
-
59
- it('is deterministic and keeps emoji and background color paired', () => {
60
- const ids = ['', 'id-2', 'id-7', 'id-13', 'id-100']
61
-
62
- ids.forEach((id) => {
63
- const emoji = getAvatarEmoji(id)
64
- const color = getAvatarBgColor(id)
65
-
66
- expect(getAvatarEmoji(id)).toBe(emoji)
67
- expect(getAvatarBgColor(id)).toBe(color)
68
- expect(emojiBgColors[emoji]).toBe(color)
69
- })
70
- })
71
32
  })
@@ -1,7 +1,6 @@
1
- import { MESSAGE_CUSTOM_TYPE } from '@linktr.ee/messaging-taxonomy'
2
- import { act, createEvent, fireEvent } from '@testing-library/react'
1
+ import { act, fireEvent } from '@testing-library/react'
3
2
  import { Channel, LocalMessage, StreamChat } from 'stream-chat'
4
- import { beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { describe, expect, it, vi } from 'vitest'
5
4
 
6
5
  import { renderWithProviders, screen } from '../../test/utils'
7
6
  import type { ChannelPreviewProps } from '../../types'
@@ -14,15 +13,9 @@ const mockUser = {
14
13
  name: 'Current User',
15
14
  }
16
15
 
17
- const mockChatContext = vi.hoisted(() => ({
18
- current: {
19
- client: { userID: 'current-user' },
20
- } as { client?: { userID?: string } },
21
- }))
22
-
23
16
  vi.mock('stream-chat-react', async (importOriginal) => ({
24
17
  ...(await importOriginal<typeof import('stream-chat-react')>()),
25
- useChatContext: () => mockChatContext.current,
18
+ useChatContext: () => ({ client: { userID: 'current-user' } }),
26
19
  }))
27
20
 
28
21
  type MockChannel = Channel & {
@@ -80,406 +73,6 @@ describe('CustomChannelPreview', () => {
80
73
  onChannelSelect: vi.fn(),
81
74
  }
82
75
 
83
- beforeEach(() => {
84
- mockChatContext.current = {
85
- client: { userID: 'current-user' },
86
- }
87
- defaultProps.onChannelSelect.mockClear()
88
- })
89
-
90
- it('applies selected and unselected row styling from context', () => {
91
- const channel = createMockChannel([])
92
- const otherChannel = createMockChannel([])
93
- otherChannel.id = 'channel-2'
94
-
95
- const { rerender } = renderWithProviders(
96
- <ChannelListProvider
97
- value={{
98
- selectedChannel: channel,
99
- onChannelSelect: defaultProps.onChannelSelect,
100
- debug: false,
101
- }}
102
- >
103
- <CustomChannelPreview {...defaultProps} channel={channel} />
104
- </ChannelListProvider>
105
- )
106
-
107
- let row = screen.getByRole('button')
108
- expect(row).toHaveClass('bg-black/[0.04]')
109
- expect(row).not.toHaveClass('hover:bg-black/[0.02]')
110
-
111
- rerender(
112
- <ChannelListProvider
113
- value={{
114
- selectedChannel: otherChannel,
115
- onChannelSelect: defaultProps.onChannelSelect,
116
- debug: false,
117
- }}
118
- >
119
- <CustomChannelPreview {...defaultProps} channel={channel} />
120
- </ChannelListProvider>
121
- )
122
-
123
- row = screen.getByRole('button')
124
- expect(row).not.toHaveClass('bg-black/[0.04]')
125
- expect(row).toHaveClass('hover:bg-black/[0.02]')
126
-
127
- rerender(
128
- <ChannelListProvider
129
- value={{
130
- selectedChannel: otherChannel,
131
- onChannelSelect: defaultProps.onChannelSelect,
132
- debug: false,
133
- }}
134
- >
135
- <CustomChannelPreview {...defaultProps} channel={channel} unread={1} />
136
- </ChannelListProvider>
137
- )
138
- expect(screen.getByRole('button')).toHaveClass('hover:bg-black/[0.02]')
139
- })
140
-
141
- it('selects the channel by click and activation keys only', () => {
142
- const channel = createMockChannel([])
143
- const onChannelSelect = vi.fn()
144
- const { container } = renderWithProviders(
145
- <ChannelListProvider value={{ onChannelSelect, debug: false }}>
146
- <CustomChannelPreview channel={channel} />
147
- </ChannelListProvider>
148
- )
149
- const row = container.querySelector('[role="button"]') as HTMLElement
150
-
151
- fireEvent.click(row)
152
- fireEvent.keyDown(row, { key: 'Enter' })
153
- const spaceEvent = createEvent.keyDown(row, { key: ' ' })
154
- fireEvent(row, spaceEvent)
155
-
156
- expect(spaceEvent.defaultPrevented).toBe(true)
157
- expect(onChannelSelect).toHaveBeenCalledTimes(3)
158
- expect(onChannelSelect).toHaveBeenNthCalledWith(1, channel)
159
- expect(onChannelSelect).toHaveBeenNthCalledWith(2, channel)
160
- expect(onChannelSelect).toHaveBeenNthCalledWith(3, channel)
161
-
162
- fireEvent.keyDown(row, { key: 'a' })
163
- fireEvent.keyDown(row, { key: 'Enter', repeat: true })
164
- expect(onChannelSelect).toHaveBeenCalledTimes(3)
165
- })
166
-
167
- it('renders with an unhydrated client and resolves the fallback member', () => {
168
- mockChatContext.current = { client: undefined }
169
- const channel = createMockChannel([])
170
-
171
- expect(() =>
172
- renderWithProviders(
173
- <CustomChannelPreview {...defaultProps} channel={channel} />
174
- )
175
- ).not.toThrow()
176
- expect(screen.getByText('Current User')).toBeInTheDocument()
177
- })
178
-
179
- it('resolves the other participant using the current client user id', () => {
180
- const channel = createMockChannel([])
181
-
182
- renderWithProviders(
183
- <CustomChannelPreview {...defaultProps} channel={channel} />
184
- )
185
- expect(screen.getByText('Alice')).toBeInTheDocument()
186
-
187
- mockChatContext.current = { client: { userID: 'participant-1' } }
188
- renderWithProviders(
189
- <CustomChannelPreview {...defaultProps} channel={channel} />
190
- )
191
- expect(screen.getByText('Current User')).toBeInTheDocument()
192
- })
193
-
194
- it('renders safe fallbacks when channel state is empty', () => {
195
- const channel = createMockChannel([])
196
- Object.assign(channel, { state: {} })
197
-
198
- expect(() =>
199
- renderWithProviders(
200
- <CustomChannelPreview {...defaultProps} channel={channel} />
201
- )
202
- ).not.toThrow()
203
- expect(screen.getByText('Unknown member')).toBeInTheDocument()
204
- expect(screen.getByText('No messages yet')).toBeInTheDocument()
205
- expect(screen.queryByText('Starred conversation.')).not.toBeInTheDocument()
206
- })
207
-
208
- it('renders a participant image only when one is supplied', () => {
209
- const channel = createMockChannel([])
210
- channel.state.members['participant-1'].user = {
211
- id: 'participant-1',
212
- name: 'Alice',
213
- image: 'https://example.com/alice.png',
214
- }
215
-
216
- const { container, rerender } = renderWithProviders(
217
- <CustomChannelPreview {...defaultProps} channel={channel} />
218
- )
219
- expect(container.querySelectorAll('img')).toHaveLength(1)
220
- expect(container.querySelector('img')).toHaveAttribute(
221
- 'src',
222
- 'https://example.com/alice.png'
223
- )
224
-
225
- channel.state.members['participant-1'].user = {
226
- id: 'participant-1',
227
- name: 'Alice',
228
- }
229
- rerender(
230
- <CustomChannelPreview {...defaultProps} channel={channel} unread={1} />
231
- )
232
- expect(container.querySelectorAll('img')).toHaveLength(0)
233
- })
234
-
235
- it.each([
236
- [undefined, undefined],
237
- [3, '3'],
238
- [99, '99'],
239
- [100, '99+'],
240
- ])('renders the unread badge for unread=%s', (unread, expectedBadge) => {
241
- const channel = createMockChannel([])
242
- const { container } = renderWithProviders(
243
- <CustomChannelPreview
244
- {...defaultProps}
245
- channel={channel}
246
- unread={unread}
247
- />
248
- )
249
-
250
- if (expectedBadge) {
251
- expect(screen.getByText(expectedBadge)).toBeInTheDocument()
252
- } else {
253
- expect(
254
- container.querySelector('span[class*="bg-[#7f22fe]"]')
255
- ).not.toBeInTheDocument()
256
- }
257
- })
258
-
259
- it('renders a timestamp only when the last message has created_at', () => {
260
- const channel = createMockChannel([
261
- {
262
- id: 'msg-1',
263
- text: 'Recent message',
264
- type: 'regular',
265
- created_at: new Date(),
266
- user: { id: 'participant-1', name: 'Alice' },
267
- },
268
- ])
269
-
270
- const { container, rerender } = renderWithProviders(
271
- <CustomChannelPreview {...defaultProps} channel={channel} />
272
- )
273
- const timestamp = () =>
274
- container.querySelector('h3')?.parentElement?.querySelector('span')
275
-
276
- expect(timestamp()).toHaveTextContent('Just now')
277
- expect(screen.getByText('Just now')).toBeInTheDocument()
278
-
279
- Object.assign(channel.state.messages[0], { created_at: undefined })
280
- rerender(
281
- <CustomChannelPreview {...defaultProps} channel={channel} unread={1} />
282
- )
283
- expect(timestamp()).toBeNull()
284
- expect(screen.queryByText('Just now')).not.toBeInTheDocument()
285
- })
286
-
287
- it.each([
288
- [
289
- 'tip with text',
290
- {
291
- metadata: { custom_type: MESSAGE_CUSTOM_TYPE.TIP },
292
- text: 'A generous tip',
293
- },
294
- 'A generous tip',
295
- ],
296
- [
297
- 'tip without text',
298
- { metadata: { custom_type: MESSAGE_CUSTOM_TYPE.TIP } },
299
- 'Sent a tip',
300
- ],
301
- [
302
- 'paid with text',
303
- {
304
- metadata: { custom_type: MESSAGE_CUSTOM_TYPE.PAID },
305
- text: 'Paid message',
306
- },
307
- 'Paid message',
308
- ],
309
- [
310
- 'paid without text',
311
- { metadata: { custom_type: MESSAGE_CUSTOM_TYPE.PAID } },
312
- 'Sent a message',
313
- ],
314
- [
315
- 'link attachment',
316
- { attachments: [{ og_scrape_url: 'https://example.com' }] },
317
- 'https://example.com',
318
- ],
319
- [
320
- 'image attachment',
321
- { attachments: [{ type: 'image' }] },
322
- '📷 Sent an image',
323
- ],
324
- [
325
- 'video attachment',
326
- { attachments: [{ type: 'video' }] },
327
- '🎥 Sent a video',
328
- ],
329
- ['audio attachment', { attachments: [{ type: 'audio' }] }, '🎵 Sent audio'],
330
- ['file attachment', { attachments: [{ type: 'file' }] }, '📎 Sent a file'],
331
- [
332
- 'unknown attachment',
333
- { attachments: [{ type: 'unknown' }] },
334
- '📎 Sent an attachment',
335
- ],
336
- ])('renders the %s preview text', (_name, message, expectedText) => {
337
- const channel = createMockChannel([
338
- {
339
- id: 'msg-1',
340
- type: 'regular',
341
- ...message,
342
- user: { id: 'participant-1', name: 'Alice' },
343
- },
344
- ])
345
-
346
- renderWithProviders(
347
- <CustomChannelPreview {...defaultProps} channel={channel} />
348
- )
349
- expect(screen.getByText(expectedText)).toBeInTheDocument()
350
- })
351
-
352
- it('prefixes chatbot previews but not normal message previews', () => {
353
- const channel = createMockChannel([
354
- {
355
- id: 'msg-1',
356
- text: 'Automated response',
357
- type: 'regular',
358
- metadata: { custom_type: MESSAGE_CUSTOM_TYPE.CHATBOT },
359
- },
360
- ])
361
- const { rerender } = renderWithProviders(
362
- <CustomChannelPreview {...defaultProps} channel={channel} />
363
- )
364
- expect(screen.getByText('✨ Automated response')).toBeInTheDocument()
365
-
366
- channel.state.messages[0].metadata = {}
367
- channel.state.messages[0].text = 'Human response'
368
- rerender(
369
- <CustomChannelPreview {...defaultProps} channel={channel} unread={1} />
370
- )
371
- expect(screen.getByText('Human response')).toBeInTheDocument()
372
- expect(screen.queryByText('✨ Human response')).not.toBeInTheDocument()
373
- })
374
-
375
- it('uses renderMessagePreview from context with the resolved default text', () => {
376
- const channel = createMockChannel([
377
- {
378
- id: 'msg-1',
379
- text: 'Default preview',
380
- type: 'regular',
381
- },
382
- ])
383
- const renderMessagePreview = vi.fn(
384
- (_message: LocalMessage | undefined, _defaultPreview?: string) =>
385
- 'Custom preview'
386
- )
387
-
388
- renderWithProviders(
389
- <ChannelListProvider
390
- value={{ onChannelSelect: vi.fn(), debug: false, renderMessagePreview }}
391
- >
392
- <CustomChannelPreview channel={channel} />
393
- </ChannelListProvider>
394
- )
395
-
396
- expect(screen.getByText('Custom preview')).toBeInTheDocument()
397
- expect(renderMessagePreview).toHaveBeenCalledWith(
398
- channel.state.messages[0],
399
- 'Default preview'
400
- )
401
- })
402
-
403
- it('renders the default preview when renderMessagePreview is not supplied', () => {
404
- const channel = createMockChannel([
405
- {
406
- id: 'msg-1',
407
- text: 'Default preview',
408
- type: 'regular',
409
- },
410
- ])
411
-
412
- renderWithProviders(
413
- <CustomChannelPreview {...defaultProps} channel={channel} />
414
- )
415
- expect(screen.getByText('Default preview')).toBeInTheDocument()
416
- })
417
-
418
- it('uses the viewer language translation when available', () => {
419
- const channel = createMockChannel([
420
- {
421
- id: 'msg-1',
422
- text: 'Raw text',
423
- type: 'regular',
424
- i18n: { es_text: 'Texto traducido', language: 'es' },
425
- },
426
- ])
427
- const { rerender } = renderWithProviders(
428
- <ChannelListProvider
429
- value={{
430
- onChannelSelect: vi.fn(),
431
- debug: false,
432
- viewerLanguage: 'es-MX',
433
- }}
434
- >
435
- <CustomChannelPreview channel={channel} />
436
- </ChannelListProvider>
437
- )
438
- expect(screen.getByText('Texto traducido')).toBeInTheDocument()
439
-
440
- rerender(
441
- <ChannelListProvider value={{ onChannelSelect: vi.fn(), debug: false }}>
442
- <CustomChannelPreview channel={channel} />
443
- </ChannelListProvider>
444
- )
445
- expect(screen.getByText('Raw text')).toBeInTheDocument()
446
- })
447
-
448
- it('logs the preview payload only when debug is enabled', () => {
449
- const channel = createMockChannel([])
450
- const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
451
-
452
- renderWithProviders(
453
- <ChannelListProvider value={{ onChannelSelect: vi.fn(), debug: true }}>
454
- <CustomChannelPreview channel={channel} />
455
- </ChannelListProvider>
456
- )
457
- expect(consoleLog).toHaveBeenCalledWith(
458
- '📺 [ChannelList] 📋 CHANNEL PREVIEW RENDER',
459
- expect.objectContaining({
460
- channelId: channel.id,
461
- isSelected: false,
462
- participantName: 'Alice',
463
- unreadCount: 0,
464
- hasTimestamp: false,
465
- })
466
- )
467
-
468
- consoleLog.mockClear()
469
- const { rerender } = renderWithProviders(
470
- <ChannelListProvider value={{ onChannelSelect: vi.fn(), debug: false }}>
471
- <CustomChannelPreview channel={channel} />
472
- </ChannelListProvider>
473
- )
474
- expect(consoleLog).not.toHaveBeenCalled()
475
- rerender(
476
- <ChannelListProvider value={{ onChannelSelect: vi.fn(), debug: false }}>
477
- <CustomChannelPreview channel={channel} />
478
- </ChannelListProvider>
479
- )
480
- expect(consoleLog).not.toHaveBeenCalled()
481
- })
482
-
483
76
  it('uses username when participant name is UUID-like', () => {
484
77
  const channel = createMockChannel([])
485
78
  channel.state.members['participant-1'] = {