@linktr.ee/messaging-react 3.6.1 → 3.6.2-rc-1783304704

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 (34) hide show
  1. package/dist/{Card-d0_xEfdM.cjs → Card-6FOd4_j_.cjs} +2 -2
  2. package/dist/{Card-d0_xEfdM.cjs.map → Card-6FOd4_j_.cjs.map} +1 -1
  3. package/dist/{Card-DcUPqd79.cjs → Card-BhIG7T7c.cjs} +2 -2
  4. package/dist/{Card-DcUPqd79.cjs.map → Card-BhIG7T7c.cjs.map} +1 -1
  5. package/dist/{Card-BuR6mbUu.cjs → Card-CA63InbA.cjs} +2 -2
  6. package/dist/{Card-BuR6mbUu.cjs.map → Card-CA63InbA.cjs.map} +1 -1
  7. package/dist/{Card-B0lzpuYf.js → Card-C_TfROjB.js} +3 -3
  8. package/dist/{Card-B0lzpuYf.js.map → Card-C_TfROjB.js.map} +1 -1
  9. package/dist/{Card-BrC7mKtr.js → Card-DN8P8IeT.js} +2 -2
  10. package/dist/{Card-BrC7mKtr.js.map → Card-DN8P8IeT.js.map} +1 -1
  11. package/dist/{Card-CxS7GNqp.js → Card-DgWCIgWw.js} +2 -2
  12. package/dist/{Card-CxS7GNqp.js.map → Card-DgWCIgWw.js.map} +1 -1
  13. package/dist/{LockedThumbnail-BaDgF8qJ.js → LockedThumbnail-DNy4CoCm.js} +2 -2
  14. package/dist/{LockedThumbnail-BaDgF8qJ.js.map → LockedThumbnail-DNy4CoCm.js.map} +1 -1
  15. package/dist/{LockedThumbnail-BDtKMJjz.cjs → LockedThumbnail-DPlNLdpK.cjs} +2 -2
  16. package/dist/{LockedThumbnail-BDtKMJjz.cjs.map → LockedThumbnail-DPlNLdpK.cjs.map} +1 -1
  17. package/dist/index-ClREqfMm.cjs +2 -0
  18. package/dist/index-ClREqfMm.cjs.map +1 -0
  19. package/dist/{index-BPfehHDD.js → index-CxGOKFnx.js} +3786 -3876
  20. package/dist/index-CxGOKFnx.js.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.js +1 -1
  23. package/package.json +2 -2
  24. package/src/components/ChannelHeaderRedesign.tsx +231 -0
  25. package/src/components/ChannelView.stories.tsx +68 -11
  26. package/src/components/ChannelView.test.tsx +54 -13
  27. package/src/components/ChannelView.tsx +74 -28
  28. package/src/components/CustomMessage/index.tsx +0 -67
  29. package/dist/index-BPfehHDD.js.map +0 -1
  30. package/dist/index-G4pnfm4m.cjs +0 -2
  31. package/dist/index-G4pnfm4m.cjs.map +0 -1
  32. package/src/components/CustomMessage/StreamAttachmentMessage.stories.tsx +0 -178
  33. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +0 -186
  34. package/src/components/CustomMessage/StreamAttachmentMessage.tsx +0 -417
@@ -1,417 +0,0 @@
1
- import React from 'react'
2
- import type { Attachment } from 'stream-chat'
3
-
4
- import LinkAttachment from '../LinkAttachment'
5
- import { isPlayableMedia } from '../LinkAttachment/components/_shared/CardThumbnail'
6
- import MessageAttachment from '../MessageAttachment'
7
- import type { BubbleGroupPosition } from '../MessageAttachment/types'
8
-
9
- type StreamMediaKind = 'image' | 'video' | 'audio' | 'pdf' | 'file'
10
-
11
- export type AttachmentSegment =
12
- | { type: 'link'; attachments: Attachment[] }
13
- | { type: 'media'; kind: StreamMediaKind; attachments: Attachment[] }
14
-
15
- type StreamThreadMessage = {
16
- attachments?: Attachment[]
17
- text?: string
18
- }
19
-
20
- const SENT_CAPTION_CLASS =
21
- 'mt-[2px] max-w-[280px] whitespace-pre-wrap break-words rounded-[18px] bg-[#121110] px-4 py-2 text-white'
22
- const RECEIVED_CAPTION_CLASS =
23
- 'mt-[2px] max-w-[280px] whitespace-pre-wrap break-words rounded-[18px] bg-[#e9eaed] px-4 py-2 text-[#080707]'
24
-
25
- function trimToUndefined(value?: string | null): string | undefined {
26
- const trimmed = value?.trim()
27
- return trimmed ? trimmed : undefined
28
- }
29
-
30
- export function normalizeLinkUrl(url: string): string {
31
- return url
32
- .trim()
33
- .replace(/^https?:\/\//i, '')
34
- .replace(/\/+$/, '')
35
- .toLowerCase()
36
- }
37
-
38
- export function linkCaptionDuplicatesAttachmentUrl(
39
- caption: string | undefined,
40
- attachmentUrl?: string
41
- ): boolean {
42
- if (!caption?.trim() || !attachmentUrl?.trim()) {
43
- return false
44
- }
45
-
46
- const normalizedCaption = normalizeLinkUrl(caption)
47
- const normalizedUrl = normalizeLinkUrl(attachmentUrl)
48
-
49
- if (normalizedCaption === normalizedUrl) {
50
- return true
51
- }
52
-
53
- if (!normalizedCaption.includes(normalizedUrl)) {
54
- return false
55
- }
56
-
57
- const remainder = normalizedCaption.split(normalizedUrl).join('')
58
- return !/[^\p{P}\p{Z}\p{C}]/u.test(remainder)
59
- }
60
-
61
- export function linkCardPropsFromStreamAttachment(a: Attachment) {
62
- const url = trimToUndefined(a.og_scrape_url) ?? trimToUndefined(a.title_link)
63
- const thumbnailUrl =
64
- trimToUndefined((a as { image_url?: string }).image_url) ??
65
- trimToUndefined((a as { thumb_url?: string }).thumb_url)
66
- const sourceUrl = trimToUndefined(a.asset_url)
67
- const mimeType = trimToUndefined(a.mime_type)
68
-
69
- return {
70
- title: trimToUndefined((a as { title?: string }).title),
71
- description: trimToUndefined((a as { text?: string }).text),
72
- url,
73
- thumbnailUrl,
74
- // Playable clip embedded in a link preview — lets LinkAttachment render its
75
- // native video/audio player in the hero instead of a static thumbnail.
76
- sourceUrl,
77
- layout:
78
- thumbnailUrl || isPlayableMedia(mimeType, sourceUrl)
79
- ? 'featured'
80
- : 'classic',
81
- mimeType,
82
- } as const
83
- }
84
-
85
- export function getAttachmentMediaKind(
86
- attachment: Attachment
87
- ): StreamMediaKind | null {
88
- if (isLinkAttachment(attachment)) return null
89
-
90
- if (attachment.type === 'image') {
91
- const imageUrl = (attachment as { image_url?: string }).image_url
92
- if (imageUrl || attachment.asset_url) return 'image'
93
- return null
94
- }
95
-
96
- if (attachment.type === 'video' && attachment.asset_url) return 'video'
97
- if (attachment.type === 'audio' && attachment.asset_url) return 'audio'
98
-
99
- if (attachment.type === 'file' && attachment.asset_url) {
100
- if (attachment.mime_type === 'application/pdf') return 'pdf'
101
- if (attachment.mime_type?.startsWith('audio/')) return 'audio'
102
- return 'file'
103
- }
104
-
105
- return null
106
- }
107
-
108
- export function isLinkAttachment(a: Attachment): boolean {
109
- const ogScrapeUrl = trimToUndefined(a.og_scrape_url)
110
- return a.type === 'link' || (!!ogScrapeUrl && !a.asset_url)
111
- }
112
-
113
- export function buildOrderedAttachmentSegments(
114
- attachments: Attachment[] | undefined
115
- ): AttachmentSegment[] {
116
- if (!attachments?.length) return []
117
-
118
- const segments: AttachmentSegment[] = []
119
-
120
- const pushOrMergeMedia = (kind: StreamMediaKind, attachment: Attachment) => {
121
- const last = segments[segments.length - 1]
122
- if (last?.type === 'media' && last.kind === kind) {
123
- last.attachments.push(attachment)
124
- return
125
- }
126
-
127
- segments.push({ type: 'media', kind, attachments: [attachment] })
128
- }
129
-
130
- const pushOrMergeLink = (attachment: Attachment) => {
131
- const last = segments[segments.length - 1]
132
- if (last?.type === 'link') {
133
- last.attachments.push(attachment)
134
- return
135
- }
136
-
137
- segments.push({ type: 'link', attachments: [attachment] })
138
- }
139
-
140
- for (const attachment of attachments) {
141
- if (isLinkAttachment(attachment)) {
142
- pushOrMergeLink(attachment)
143
- continue
144
- }
145
-
146
- const kind = getAttachmentMediaKind(attachment)
147
- if (!kind) continue
148
-
149
- pushOrMergeMedia(kind, attachment)
150
- }
151
-
152
- return segments
153
- }
154
-
155
- export function countAttachmentSegmentBubbles(
156
- segments: AttachmentSegment[]
157
- ): number {
158
- let count = 0
159
-
160
- for (const segment of segments) {
161
- count += segment.type === 'link' ? segment.attachments.length : 1
162
- }
163
-
164
- return count
165
- }
166
-
167
- export function countMediaSegmentBubbles(segments: AttachmentSegment[]): number {
168
- return segments.filter((segment) => segment.type === 'media').length
169
- }
170
-
171
- export function bubblePositionInRun(
172
- index: number,
173
- total: number,
174
- streamPosition: BubbleGroupPosition
175
- ): BubbleGroupPosition {
176
- if (total <= 1) return streamPosition
177
- if (index === 0) return 'first'
178
- if (index === total - 1) return 'end'
179
- return 'middle'
180
- }
181
-
182
- function imageSrcFromAttachment(attachment: Attachment): string {
183
- return (
184
- trimToUndefined((attachment as { image_url?: string }).image_url) ??
185
- trimToUndefined(attachment.asset_url) ??
186
- ''
187
- )
188
- }
189
-
190
- function messageAttachmentNamespaceForKind(kind: StreamMediaKind) {
191
- switch (kind) {
192
- case 'image':
193
- return MessageAttachment.Image
194
- case 'video':
195
- return MessageAttachment.Video
196
- case 'audio':
197
- return MessageAttachment.Audio
198
- case 'pdf':
199
- return MessageAttachment.Pdf
200
- default:
201
- return MessageAttachment.File
202
- }
203
- }
204
-
205
- function buildMediaClusterRenderProps(
206
- kind: StreamMediaKind,
207
- attachments: Attachment[]
208
- ): Record<string, unknown> {
209
- if (attachments.length > 1) {
210
- switch (kind) {
211
- case 'image':
212
- return {
213
- items: attachments.map((attachment) => ({
214
- src: imageSrcFromAttachment(attachment),
215
- alt: trimToUndefined((attachment as { title?: string }).title),
216
- })),
217
- }
218
- case 'video':
219
- return {
220
- items: attachments.map((attachment) => ({
221
- src: trimToUndefined(attachment.asset_url) ?? '',
222
- poster: trimToUndefined(
223
- (attachment as { thumb_url?: string }).thumb_url
224
- ),
225
- mimeType: trimToUndefined(attachment.mime_type),
226
- })),
227
- }
228
- case 'audio':
229
- return {
230
- items: attachments.map((attachment) => ({
231
- src: trimToUndefined(attachment.asset_url) ?? '',
232
- mimeType:
233
- trimToUndefined(attachment.mime_type) ?? 'audio/mpeg',
234
- filename: trimToUndefined(
235
- (attachment as { title?: string }).title
236
- ),
237
- })),
238
- }
239
- case 'pdf':
240
- return {
241
- items: attachments.map((attachment) => ({
242
- src: trimToUndefined(attachment.asset_url) ?? '',
243
- filename: trimToUndefined(
244
- (attachment as { title?: string }).title
245
- ),
246
- fileSize: (attachment as { file_size?: number }).file_size,
247
- })),
248
- }
249
- case 'file':
250
- return {
251
- items: attachments.map((attachment) => ({
252
- src: trimToUndefined(attachment.asset_url) ?? '',
253
- filename: trimToUndefined(
254
- (attachment as { title?: string }).title
255
- ),
256
- fileSize: (attachment as { file_size?: number }).file_size,
257
- mimeType:
258
- trimToUndefined(attachment.mime_type) ??
259
- 'application/octet-stream',
260
- })),
261
- }
262
- }
263
- }
264
-
265
- const attachment = attachments[0]
266
- if (!attachment) return {}
267
-
268
- switch (kind) {
269
- case 'image':
270
- return {
271
- src: imageSrcFromAttachment(attachment),
272
- alt: trimToUndefined((attachment as { title?: string }).title),
273
- }
274
- case 'video':
275
- return {
276
- src: trimToUndefined(attachment.asset_url) ?? '',
277
- poster: trimToUndefined((attachment as { thumb_url?: string }).thumb_url),
278
- mimeType: trimToUndefined(attachment.mime_type),
279
- }
280
- case 'audio':
281
- return {
282
- src: trimToUndefined(attachment.asset_url) ?? '',
283
- mimeType: trimToUndefined(attachment.mime_type) ?? 'audio/mpeg',
284
- filename: trimToUndefined((attachment as { title?: string }).title),
285
- }
286
- case 'pdf':
287
- return {
288
- src: trimToUndefined(attachment.asset_url) ?? '',
289
- filename: trimToUndefined((attachment as { title?: string }).title),
290
- fileSize: (attachment as { file_size?: number }).file_size,
291
- }
292
- case 'file':
293
- return {
294
- src: trimToUndefined(attachment.asset_url) ?? '',
295
- filename: trimToUndefined((attachment as { title?: string }).title),
296
- fileSize: (attachment as { file_size?: number }).file_size,
297
- mimeType:
298
- trimToUndefined(attachment.mime_type) ?? 'application/octet-stream',
299
- }
300
- }
301
- }
302
-
303
- function MediaClusterBubble({
304
- groupPosition,
305
- isMyMessage,
306
- segment,
307
- text,
308
- }: {
309
- groupPosition: BubbleGroupPosition
310
- isMyMessage: boolean
311
- segment: Extract<AttachmentSegment, { type: 'media' }>
312
- text?: string
313
- }) {
314
- const TypeNamespace = messageAttachmentNamespaceForKind(segment.kind)
315
- const props = {
316
- ...buildMediaClusterRenderProps(segment.kind, segment.attachments),
317
- groupPosition,
318
- ...(text ? { text } : {}),
319
- }
320
-
321
- if (isMyMessage) {
322
- return <TypeNamespace.Sent {...props} />
323
- }
324
-
325
- return <TypeNamespace.Received {...props} />
326
- }
327
-
328
- export interface StreamAttachmentMessageProps {
329
- groupPosition: BubbleGroupPosition
330
- isMyMessage: boolean
331
- message: StreamThreadMessage
332
- }
333
-
334
- const StreamAttachmentMessage: React.FC<StreamAttachmentMessageProps> = ({
335
- groupPosition,
336
- isMyMessage,
337
- message,
338
- }) => {
339
- const segments = buildOrderedAttachmentSegments(message.attachments)
340
- if (segments.length === 0) return null
341
-
342
- const totalBubbles = countAttachmentSegmentBubbles(segments)
343
- const totalMediaBubbles = countMediaSegmentBubbles(segments)
344
- const rawCaption = message.text?.trim()
345
- const linkSegment = segments.find((segment) => segment.type === 'link')
346
- const primaryLinkUrl =
347
- linkSegment?.type === 'link'
348
- ? linkCardPropsFromStreamAttachment(linkSegment.attachments[0] ?? {}).url
349
- : undefined
350
- const caption =
351
- rawCaption &&
352
- linkCaptionDuplicatesAttachmentUrl(rawCaption, primaryLinkUrl)
353
- ? undefined
354
- : rawCaption
355
-
356
- let bubbleIndex = 0
357
- let mediaBubbleIndex = 0
358
- const rows: React.ReactNode[] = []
359
-
360
- const pushLinkRow = (attachment: Attachment, key: string) => {
361
- const isLastBubble = bubbleIndex === totalBubbles - 1
362
- bubbleIndex += 1
363
-
364
- const Card = isMyMessage ? LinkAttachment.Sent : LinkAttachment.Received
365
-
366
- rows.push(
367
- <React.Fragment key={key}>
368
- <Card {...linkCardPropsFromStreamAttachment(attachment)} />
369
- {isLastBubble && caption ? (
370
- <div className={isMyMessage ? SENT_CAPTION_CLASS : RECEIVED_CAPTION_CLASS}>
371
- {caption}
372
- </div>
373
- ) : null}
374
- </React.Fragment>
375
- )
376
- }
377
-
378
- const pushMediaCluster = (
379
- segment: Extract<AttachmentSegment, { type: 'media' }>,
380
- key: string
381
- ) => {
382
- const isLastBubble = bubbleIndex === totalBubbles - 1
383
- const position = bubblePositionInRun(
384
- mediaBubbleIndex,
385
- totalMediaBubbles,
386
- groupPosition
387
- )
388
-
389
- bubbleIndex += 1
390
- mediaBubbleIndex += 1
391
-
392
- rows.push(
393
- <MediaClusterBubble
394
- key={key}
395
- groupPosition={position}
396
- isMyMessage={isMyMessage}
397
- segment={segment}
398
- text={isLastBubble ? caption : undefined}
399
- />
400
- )
401
- }
402
-
403
- segments.forEach((segment, segmentIndex) => {
404
- if (segment.type === 'link') {
405
- segment.attachments.forEach((attachment, attachmentIndex) => {
406
- pushLinkRow(attachment, `link-${segmentIndex}-${attachmentIndex}`)
407
- })
408
- return
409
- }
410
-
411
- pushMediaCluster(segment, `media-${segmentIndex}`)
412
- })
413
-
414
- return <>{rows}</>
415
- }
416
-
417
- export default StreamAttachmentMessage